JaVa
   

Testing with Dummies

Neither of the two test approaches discussed so far in this chapter is particularly suitable for test-first development. The dependence of servlets upon other relatively complex interfaces makes it much more difficult for us to simply get going as in the development of other objects. On the top layer, we have the interfaces HttpServletRequest, HttpServletResponse, and ServletConfig. These interfaces, in turn, point to other interfaces, namely HttpSession, ServletContext, and RequestDispatcher. [4] With a little effort we should be able to replace this handful of interfaces with dummy objects. Let's plunge into a new example. Assume that a login servlet should meet the following specification:

Let's first deal with the GET request. A first test should check whether or not the page returns correct HTML with a request to enter the password:

import utmj.servlet.*;
public class LoginServletTest extends TestCase {
 public void testGet() throws Exception {
 LoginServlet servlet = new LoginServlet();
 DummyServletConfig config = new DummyServletConfig();
 servlet.init(config);
 DummyHttpServletRequest request =
 new DummyHttpServletRequest();
 DummyHttpServletResponse response =
 new DummyHttpServletResponse();
 servlet.doGet(request, response);
 DummyWebResponse webResponse = response.getWebResponse();
 assertTrue(webResponse.getText().indexOf(
 "Enter name and password to log in.") != -1);
 servlet.destroy();
 }
}


To simulate the servlet environment, we use simple dummy objects from the package utmj.servlet, which can be downloaded from the companion Web site to this tutorial. [5] Our decision in disfavor of mock objects originates from habit and is a matter of taste. [6] We can see that the behavior of a servlet engine is simulated in this way: first instantiate the servlet, then initialize it, and finally destroy it at the end of the test. The class DummyWebResponse is a specific subclass of HttpUnit's abstract WebResponse class. This is necessary because HttpUnit does not make its specific subclasses public. We are now ready to complete our test and do a refactoring at the same time:

import com.meterware.httpunit.*;
import utmj.servlet.*;
public class LoginServletTest extends TestCase {
 private LoginServlet servlet;
 private DummyHttpServletRequest request;
 private DummyHttpServletResponse response;
 private String getAction(WebForm form) {...}
 private String getMethod(WebForm form) {...}
 protected void setUp() throws Exception {
 servlet = new LoginServlet();
 servlet.init(new DummyServletConfig());
 request = new DummyHttpServletRequest();
 response = new DummyHttpServletResponse();
 }
 protected void tearDown() throws Exception {
 servlet.destroy();
 }
 public void testGet() throws Exception {
 servlet.doGet(request, response);
 DummyWebResponse webResponse = response.getWebResponse();
 assertTrue(webResponse.getText().indexOf(
 "Enter name and password to log in.") != -1);
 WebForm form = webResponse.getFormWithName("loginForm");
 assertEquals("action",
 LoginServlet.SERVLET_URI, this.getAction(form));
 assertEquals("method", "post", this.getMethod(form));
 assertEquals("name input field",
 1, form.getParameterValues("name").length);
 assertEquals("password input field",
 1, form.getParameterValues("password").length);
 assertEquals("password input type",
 "password", this.getInputType(form, "password"));
 assertNotNull("login button",
 form.getSubmitButton("loginButton"));
 }
}


To test the action and method attributes and the input type of the parameters, we had to write helper methods. Actually, we would expect the existence of these helper methods in HttpUnit's WebForm class. [7] But let's continue in our example; we will now test the POST request behavior. The user database is accessed over an interface to verify the name and password:

public interface UserDatabase {
 boolean verify(String name, String password);
}


At least two test cases are now required: (a) one with a final, and (b) one with an invalid name-password combination. First the valid combination:

public void testPostValidUser() throws Exception {
 UserDatabase mockDb = new UserDatabase() {
 public boolean verify(String name, String password) {
 assertEquals("myname", name);
 assertEquals("mypassword", password);
 return true;
 }
 };
 servlet.getServletContext().
 setAttribute("userDatabase", mockDb);
 request.addParameter("name", "myname");
 request.addParameter("password", "mypassword");
 servlet.doPost(request, response);
 DummyRequestDispatcher dispatcher =
 ((DummyServletContext) servlet.getServletContext()).
 getRequestDispatcher();
 assertEquals("Forward path",
 LoginServlet.FORWARD_URI, dispatcher.getPath());
 assertTrue("Forward called", dispatcher.forwardCalled());
 assertNotNull("Session created", request.getSession(false));
}


Notice that several things are happening here. At the beginning of the test, we build a mock object of the type UserDatabase on the fly. We then insert this database as an attribute into the servlet context. The verification at the end consists of three parts: verifying the RequestDispatcher path, invoking the forward method, and creating an HttpSession instance. The entire dummy functionality is already included in the utmj.servlet package. On the other hand, building our own mock objects would surely be justified, because at least their basic functionality would remain the same for all servlets and could be reused. Our second test case with an invalid password is very similar to the first one in that we also do some refactoring to remove duplicate code:

import utmj.servlet.*;
public class LoginServletTest extends TestCase {
 ...
 private void assertRequestDispatcher(String expectedPath) {
 DummyRequestDispatcher dispatcher =
 ((DummyServletContext) servlet.getServletContext()).
 getRequestDispatcher();
 assertEquals("Forward path",
 expectedPath, dispatcher.getPath());
 assertTrue("Forward called", dispatcher.forwardCalled());
 }
 private UserDatabase createMockDatabase(
 final String expectedName,
 final String expectedPassword,
 final boolean verify) {
 return new UserDatabase() {
 public boolean verify(String name, String password) {
 assertEquals(expectedName, name);
 assertEquals(expectedPassword, password);
 return verify;
 }
 };
 }
 private void setMockDatabase(String expectedName,
 String expectedPassword, boolean verify) {
 UserDatabase mockDb = this.createMockDatabase(
 expectedName, expectedPassword, verify);
 servlet.getServletContext().
 setAttribute("userDatabase", mockDb);
 }
 public void testPostUnvalidUser() throws Exception {
 this.setMockDatabase("wrongname", "wrongpassword", false);
 request.addParameter("name", "wrongname");
 request.addParameter("password", "wrongpassword");
 servlet.doPost(request, response);
 assertRequestDispatcher(LoginServlet.SERVLET_URI);
 assertNull("No session created", request.getSession(false));
 }
 public void testPostValidUser() throws Exception {
 this.setMockDatabase("myname", "mypassword", true);
 request.addParameter("name", "myname");
 request.addParameter("password", "mypassword");
 servlet.doPost(request, response);
 assertRequestDispatcher(LoginServlet.FORWARD_URI);
 assertNotNull("Session created", request.getSession(false));
 }
}


Toward the end, the test case testPostUnvalidUser() verifies whether or not a forward is sent to the servlet itself and to ensure that no session instance was created. Note that the redundancy in both test methods was not entirely removed, because the author believes that this makes the test logic easier to understand. The servlet implementation looks like the following:

public class LoginServlet extends HttpServlet {
 public final static String SERVLET_URI = "LoginServlet";
 public final static String FORWARD_URI = "something.jsp";
 private void forwardTo(HttpServletRequest request,
 HttpServletResponse response, String path)
 throws IOException, ServletException {
 RequestDispatcher dispatcher = this.getServletContext().
 getRequestDispatcher(path);
 dispatcher.forward(request, response);
 }
 private UserDatabase getUserDatabase() {
 return (UserDatabase) this.getServletContext().
 getAttribute("userDatabase");
 }
 private boolean verifyPassword(HttpServletRequest request)
 throws ServletException, IOException {
 UserDatabase database = this.getUserDatabase();
 String name = request.getParameter("name");
 String password = request.getParameter("password");
 return database.verify(name, password);
 }
 protected void doGet(HttpServletRequest request,
 HttpServletResponse response)
 throws ServletException, IOException {
 response.setContentType("text/html");
 PrintWriter writer = response.getWriter();
 // write HTML login page...
 writer.close();
 }
 protected void doPost(HttpServletRequest request,
 HttpServletResponse response)
 throws ServletException, IOException {
 if (this.verifyPassword(request)) {
 request.getSession();
 this.fowardTo(request, response, FORWARD_URI);
 } else {
 this.fowardTo(request, response, SERVLET_URI);
 }
 }
}


The important thing is that this servlet is not quite ready to run in a browser, because the "real" implementation of UserDatabase is missing. This means that testing with HttpUnit is ruled out. In fact, we managed to enable a test-first approach here, although by use of relatively complex dummies. [4]The servlet API Version 2.2 is our reference point here. [5]An approach similar to our dummy objects can be followed by using ServletUnit, a simulated servlet container which comes with HttpUnit. [6]Suitable servlet mock objects are included in the mock object package at [URL:Mock-Objects]. [7]Perhaps one of our readers feels like participating in the further development of Http-Unit?


JaVa
   
Comments