Information about http://www.objectmentor.com/resources/articles/Working_Backwards.pdf

10 retpahC sd ra wk c aB gn ik ro W Our…

Tags: addison wesley, assertion, database entries, e mail address, gn, ing, newkirk, nobody home, ra, robert c martin, servlet class, servlets, special test, test case, test cases, test data, test entries, unit tests, ward cunningham, wk,
Pages: 14
Language: english
Created: Mon Feb 21 16:11:20 2000
Display cached document
Page 1
image
Page 2
image
Page 3
image
Page 4
image
Page 5
image
Page 6
image
Page 7
image
Page 8
image
Page 9
image
Page 10
image
Page 11
image
Page 12
image
Page 13
image
Page 14
image
10 retpahC



                      sd ra wk c aB gn ik ro W

     Our next task was Task 5:Forgot Password Task. In this task we
 needed to respond to a user who had forgotten his password. The
 idea was to put up a simple page that prompted the user for his E-
 mail address. This page would invoke a servlet that looked up the
 user's password and emailed it to him.
     We still had not solved the problem of testing code in servlets.
 But we had learned from our past errors that servlets don't stay
 simple for long and need unit tests. Therefore we decided to put
 the bulk of the implementation in a testable, non-servlet, class
 that would be called by the servlet.

 TestNoUser
     In thinking about the test cases we realized the most interest-
 ing ones were those in which we were trying to recover from
 either a database or mailer problem. For example, the user's E-
 mail address doesn't exist; or his E-mail address confuses the
 mailer.
     In previous tasks we had loaded the database with special test
 entries. This forced us to go to extra effort to maintain these
 entries. Tests could give false results if the database entries were
 not properly managed. (See "Hit the button twice.," on
 page 70).
     It would be better if we could completely control the test data
 and conditions from within the test case. We wondered how we
 could do this?

    This is a preliminary chapter from XP in Practice by James Newkirk and Robert C.
    Martin, Copyright 2001 by Addison Wesley. All rights reserved.
    Ward Cunningham recommends writing the code you want
to see without worrying about the infrastructure that supports it.
What did we want the tests to look like? Consider the test case
where the user enters an E-mail address that is not present in the
database. We wrote the following:

   assert(remind("nobody@home")==NOEMAILFOUND);

Based upon this assertion, it was clear to us that the remind
function's responsibilities were to look up the E-mail address in
the database and then send the user the reminder message.
    However, this particular test could be thwarted if somebody
accidentally put nobody@home into the database. We needed a
way to isolate the test case from the database.

Spoofing. Rather than allowing PasswordReminder direct
access to the database, we created an interface that Password-
Reminder could use to access the database. Then, inside the test
program, we created an anonymous inner class that implemented
that interface and always returned null.(See Listings 34 and 35.)
   Listing 34
   PasswordReminderDatabase
   public interface PasswordReminderDatabase
   {
     public String findPasswordFromEmail(String email);
   }

   Listing 35
   ForgotPasswordTest.testNoUser refactoring 1.
   public void testNoUser()
     {
       String noUser = "nobody@home";
       PasswordReminderDatabase db =
         new PasswordReminderDatabase()
       {
         public String findPasswordFromEmail(String email)
         {
           return null;
         }




                                                                98
     Listing 35 (Continued)
     ForgotPasswordTest.testNoUser refactoring 1.
           };
           PasswordReminder pr = new PasswordReminder(db);
           assert(pr.remind(noUser)==
               PasswordReminder.NOEMAILFOUND);
         } // testNoUser

    This technique is called spoofing. We fooled the Password-
Reminder into thinking it had a real database when in reality we
completely replaced the database with a mock-object that provided
the behavior that we wanted to test.
    This wasn't compiling yet, because we hadn't written Pass-
wordReminder. But we were getting a good picture of what
PasswordReminder should look like. So we wrote it. (See List-
ing 36.)
     Listing 36
     PasswordReminder refactoring 1.
     public class PasswordReminder
     {
       public static final int OK = 0;
       public static final int NOEMAILFOUND = 1;

         private PasswordReminderDatabase itsDb;
         public PasswordReminder(PasswordReminderDatabase db)
         {
           itsDb = db;
         }

         public int remind(String email)
         {
           int status;
           String password = itsDb.findPasswordFromEmail(email);
           if (password != null)
             status = OK;
           else
             status = NOEMAILFOUND;
             return status;
         }

     }

    Clearly this function is incomplete. It doesn't send the
reminder E-mail message if the database access succeeds. But one
step at a time. Right now we have a green bar!
              Exploration
                                                               Release 1:
                                                               Hours
         0         8        16   24   32   40   48   56   60
99
Working Backwards. Notice the order in which we did this. We
started with the initial assertion. We built the test case around
that assertion which helped us design the PasswordReminder.
Then, finally, we wrote the incomplete version of Password-
Reminder. We backed into the solution starting from an initial
assertion and a set of initial constraints. This kept us from guess-
ing about anything. Instead of anticipating and inventing the
infrastructure we needed, or the methods we needed, or the
objects we needed, we waited until the need was obvious.

But we weren't done. We may not have been done with Pass-
wordReminder, but we were done with the current test case. We
weren't going to write any more code into PasswordReminder
until we had a failing test case.

TestGoodEmail

Next we wrote the test case that demonstrated that we could send
the reminder message. We wanted that test case to look some-
thing like this:
    assertEquals(PasswordReminder.OK,
                    pr.remind("Bob"));
    assertEquals("Bob", theToAddr);
    assertEquals("Your Object Mentor Password",
                    theSubj);
    assertEquals(
      "Your Object Mentor Password is saba",
      theBody);
For the purposes of this test, we could assume that an OK return
indicated that the mail was successfully sent. The rest of the
asserts simply inspected the message to make sure it was properly
formed.
    We didn't want PasswordReminder to pass the E-mail mes-
sage back to its caller. First, nobody other than the test was inter-
ested in that message. Second, in order to deal with the message
there would have had to be more code in the servlet. And we
didn't want to put code in places that were hard to test.
    Also, this unit test was not intended to test that mail actually
got sent. Rather its intent was to ensure that RemindPassword
had the correct logic. It would have been a pain to have E-mail
get sent every time we ran this test.
    Both problems can be solved by spoofing the mailer. We cre-
ated a Mailer interface, passed it into the PasswordReminder,
and implemented the test case as shown in Listings 37 and 38.
   Listing 37
   Mailer, refactoring 1.
    public interface Mailer
    {
      public void send(String to, String subj, String body);
    }

   Listing 38
   ForgotPasswordTest refactoring 2.
    public class ForgotPasswordTest extends TestCase
    {

      private String theToAddr = "";
      private String theSubj = "";
      private String theBody = "";

      public void         setup()
      {
        theToAddr         = "";
        theSubj =         "";
        theBody =         "";
      }

      public void testGoodEmail()
      {
        PasswordReminderDatabase db =
           new PasswordReminderDatabase()
        {
           public String findPasswordFromEmail(String email)
           {return "saba";}
        };

           Mailer m = new Mailer()
           {
             public boolean send(String to, String subj,
                                 String body)
             {
               theToAddr = to;

            Exploration
                                                                Release 1:
                                                                Hours
       0         8        16      24   32   40   48   56   60
101
   Listing 38 (Continued)
   ForgotPasswordTest refactoring 2.
                  theSubj = subj;
                  theBody = body;
              }
            };
            PasswordReminder pr = new PasswordReminder(db, m);
            assertEquals(PasswordReminder.OK, pr.remind("Bob"));
            assertEquals("Bob", theToAddr);
            assertEquals("Your Object Mentor Password", theSubj);
            assertEquals("Your Object Mentor Password is saba",
                         theBody);
        }
    }

    Notice that the mock-mailer loads the instance variables of
the test case with the data from the message. This allows the test
case to later assert that their values are correct. All that this really
tests is that PasswordReminder.remind actually calls
Mailer.send with the right arguments.
    Next we added the Mailer argument to PasswordRemin-
der.remind that would make this test case compile. Upon test-
ing, the bar went red, as expected.
    Next we added the code to PasswordReminder.remind
that made the test pass. See Listing 39.
   Listing 39
   PasswordReminder refactoring 2.
    public class PasswordReminder
    {
      public static final int OK = 0;
      public static final int NOEMAILFOUND = 1;

        private PasswordReminderDatabase itsDb;
        private Mailer itsMailer;
        public PasswordReminder(PasswordReminderDatabase db,
                                Mailer m)
        {
          itsDb = db;
          itsMailer = m;
        }
        public int remind(String email)
        {
          int status = OK;
          String password = itsDb.findPasswordFromEmail(email);
   Listing 39 (Continued)
   PasswordReminder refactoring 2.
            if (password != null)
            {
              itsMailer.send(
                 email,
                 "Your Object Mentor Password",
                 "Your Object Mentor Password is " + password);
            }
            else
              status = NOEMAILFOUND;
            return status;
        }
    }

TestBadEmail
   Our last test case involved a failure of the mailer to deal with
an E-mail address. Once again we started with an assertion.
   assertEquals(PasswordReminder.EMAILERROR,
                     pr.remind("Bob"));
Next we changed Mailer.send to return a boolean, and spoofed
it to return false in this test case. Finally we added EMAILERROR
to PasswordReminder.
    The tests failed.
    Next we changed PasswordReminder.remind to deal with
the boolean return value from Mailer.
    The tests passed. We couldn't think of any more test cases. We
were done with the non-servlet code. See Listings 40-42
   Listing 40
   Mailer, refactoring 2.
    public interface Mailer
    {
      public boolean send(String to, String subj, String body);
    }

   Listing 41
   ForgotPasswordTest, final refactoring.
    public class ForgotPasswordTest extends TestCase
    {
      public ForgotPasswordTest(String name)
      {

             Exploration
                                                              Release 1:
                                                              Hours
        0         8        16   24   32   40   48   56   60
103
Listing 41 (Continued)
ForgotPasswordTest, final refactoring.
      super(name);
  }

  public static Test suite()
  {
    return new TestSuite(ForgotPasswordTest.class);
  }

  private   boolean mailSent   = false;
  private   String theToAddr   = "";
  private   String theSubj =   "";
  private   String theBody =   "";
  public void setup()
  {
    mailSent = false;
    theToAddr = "";
    theSubj = "";
    theBody = "";
  }

  public void testNoUser()
  {
    String noUser = "IDon'tExist";
    PasswordReminderDatabase db =
      new PasswordReminderDatabase()
    {
      public String findPasswordFromEmail(String email)
      {
        return null;
      }
    };
    Mailer m = new Mailer()
    {
      public boolean send(String to, String subject,
                          String body)
      {
        mailSent = true;
        return true;
      }
    };
    PasswordReminder pr = new PasswordReminder(db, m);
    assert(pr.remind(noUser) ==
           PasswordReminder.NOEMAILFOUND);
    assert(mailSent == false);
  } // testNoUser

  public void testBadEmail()
  {
    PasswordReminderDatabase db =
      new PasswordReminderDatabase()
    {
  Listing 41 (Continued)
  ForgotPasswordTest, final refactoring.
               public String findPasswordFromEmail(String email)
               {return "saba";}
          };

          Mailer m = new Mailer()
          {
             public boolean send(String to, String subj,
                                 String body)
             {
               theToAddr = to;
               theSubj = subj;
               theBody = body;
               return false;
             }
          };

        PasswordReminder pr = new PasswordReminder(db, m);
        assertEquals(PasswordReminder.EMAILERROR,
                     pr.remind("Bob"));
        assertEquals("Bob", theToAddr);
        assertEquals("Your Object Mentor Password", theSubj);
        assertEquals("Your Object Mentor Password is saba",
                     theBody);
      } // testBadEmail

      public void testGoodEmail()
      {
        PasswordReminderDatabase db =
           new PasswordReminderDatabase()
        {
           public String findPasswordFromEmail(String email)
           {return "saba";}
        };

          Mailer m = new Mailer()
          {
             public boolean send(String to, String subj,
                                 String body)
             {
               theToAddr = to;
               theSubj = subj;
               theBody = body;
               return true;
             }
          };

          PasswordReminder pr = new PasswordReminder(db, m);
          assertEquals(PasswordReminder.OK, pr.remind("Bob"));
          assertEquals("Bob", theToAddr);




           Exploration
                                                             Release 1:
                                                             Hours
      0          8       16   24   32   40   48    56   60
105
   Listing 41 (Continued)
   ForgotPasswordTest, final refactoring.
       assertEquals("Your Object Mentor Password", theSubj);
       assertEquals("Your Object Mentor Password is saba",
                    theBody);
     }// testGoodEmail
   } // ForgotPasswordTest

   Listing 42
   PasswordReminder, final refactoring
   public class PasswordReminder
   {
     public static final int OK = 0;
     public static final int NOEMAILFOUND = 1;
     public static final int EMAILERROR = 2;

       private PasswordReminderDatabase itsDb;
       private Mailer itsMailer;
       public PasswordReminder(PasswordReminderDatabase db,
                               Mailer m)
       {
         itsDb = db;
         itsMailer = m;
       }

       public int remind(String email)
       {
         int status = OK;
         String password = itsDb.findPasswordFromEmail(email);
         if (password != null)
         {
           boolean wasSent =
             itsMailer.send(
                email,
                "Your Object Mentor Password",
                "Your Object Mentor Password is " + password);
           if (!wasSent)
              status = EMAILERROR;
         }
         else
           status = NOEMAILFOUND;
           return status;
       }
   }

Implementing the Mock-Objects

The Mailer. Next we implemented the two mock-object interfaces.
The Mailer was quite simple. First we wrote the test cases (List-
ing 43) and then we wrote SMTPMailer (Listing 44) to satisfy
those test cases.
   Listing 43
   EmailTest
   public class EmailTest extends TestCase
   {
     public EmailTest(String name)
     {
       super(name);
     }

       public static Test suite()
       {
         return new TestSuite(EmailTest.class);
       }

       public void testBadEmailAddr()
       {
         Mailer m = new SMTPMailer("mail.wwa.com");
         assert(m.send("noAt", "subj", "body")== false);
       }
       public void testBadMailHost()
       {
         Mailer m = new SMTPMailer("wwa.com");
         assert(m.send("newkirk@objectmentor.com", "subj",
                       "body")== false);
       }

       public void testSuccess()
       {
         Mailer m = new SMTPMailer("mail.wwa.com");
         assert(m.send("newkirk@objectmentor.com", "subj",
                       "body")== true);
       }

   }

   Listing 44
   SMTPMailer
   public class SMTPMailer implements Mailer
   {
     private String mailHost = null;

       public SMTPMailer(String mailHost)
       {
         this.mailHost = mailHost;
       }

       public boolean send(String to, String subj, String body)
       {

           Exploration
                                                            Release 1:
                                                            Hours
       0        8        16   24   32   40   48   56   60
107
    Listing 44 (Continued)
    SMTPMailer
            boolean sent = true;
            try
            {
              MailMessage msg = new MailMessage(mailHost);
              msg.from("info@objectmentor.com");
              msg.to(to);
              msg.setSubject(subj);
             PrintStream out = msg.getPrintStream();
             out.println(body);
             msg.sendAndClose();
            }
            catch(IOException e)
            {
              e.printStackTrace();
              sent = false;
            }

            return sent;
        }
    }

PasswordReminderDatabase. Implementing the PasswordRe-
minderDatabase was a bit more interesting. The find-
PasswordFromEmail function already exists in the Database
class. The obvious strategy to implementing Password-
ReminderDatabase is to make Database implement that
interface. However, there are two reasons not to do this.
    First, we liked the spoofing approach. We envisioned using it
quite a bit more in the future. However, we didn't want every
spoof to force us to change the Database class.
    Second, and more to the point, findUserByEmail is static
in Database. You can't put static functions in interfaces.
    So we created an ADAPTER1 called PasswordDatabase-
Adapter. This class delegated the findUserByEmail function
to the static function of Database. (See Listing 45)



1. Gamma, E., R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of
   Reusable Object Oriented Software. Reading, Mass.: Addison-Wesley, 1995.
   Listing 45
   PasswordDatabaseAdapter
    public class PasswordDatabaseAdapter implements
    PasswordReminderDatabase
    {
      public String findPasswordFromEmail(String email)
      {
        return Database.findPasswordFromEmail(email);
      }
    }

ForgotPassword Servlet
    Finally, still backing into the solution, we wrote the servlet.
It's pretty simple. It creates the necessary objects, invokes
remind, and deals with the status. (See Listing 46.)
   Listing 46
   ForgotPassword Servlet
    public class ForgotPassword extends HttpServlet
    {
      private HttpServletRequest request;
      private HttpServletResponse response;
      private String email;
      private String url;

      public void doGet(HttpServletRequest req,
                        HttpServletResponse resp)
        throws ServletException, IOException
      {
        request = req;
        response = resp;
        email = (String)request.getParameter("email");
        url = (String)request.getParameter("url");
        PasswordReminderDatabase db =
          new PasswordDatabaseAdapter();
        Mailer m =
          new SMTPMailer(getInitParameter("mail-server"));
        PasswordReminder pr = new PasswordReminder(db, m);
        int status = pr.remind(email);
        if (status == PasswordReminder.OK)
          redirect("sent");
        else if (status == PasswordReminder.EMAILERROR)
          redirect("bademail");
        else if (status == PasswordReminder.NOEMAILFOUND)
          redirect("noemail");
      }

      private void redirect(String base)
        throws ServletException, IOException
      {

          Exploration
                                                           Release 1:
                                                           Hours
      0        8        16   24   32   40   48   56   60
109
   Listing 46 (Continued)
   ForgotPassword Servlet
            String baseURI = getInitParameter(base);
            String uri =
              baseURI + "?email=" + email + "&url=" + url;
            response.sendRedirect(uri);
        }
        public void doPost(HttpServletRequest request,
                           HttpServletResponse response)
          throws ServletException, IOException
        {
          doGet(request, response);
        }
    }

    There appears to be some duplication between this servlet and
RedirectingServlet. We leave that refactoring as an exercise for
the reader ;^).

Conclusion
    After we got the servlet in place, we ran our manual accep-
tance tests. Everything worked as planned.
    The total time spent on this task was about four hours instead
of the six that we had estimated.
    The most important lesson learned in this task was the prac-
tice of backing into solutions from initial premises and con-
straints. The technique of using mock-objects to keep all the test
related information inside the test cases was very useful, and also
helped us maintain our backwards thinking.