Friday, January 20, 2006

More Mockery

In my last post I described using Spring's mock request and response classes to unit test controllers. The examples given were very simple. In real situations, controllers typically depend on other resources, often to provide data sources etc. These can also be troublesome to test with, and cause tests to be unpredictable and slow.

To combat this, and create fast, decoupled unit tests, I have been using JMock in tandem with the Spring mock library. This allows very expressive tests to be created, which run independently of any actual data source dependencies, and have predicatable results.

For example, say we are writing a controller to list the avaiable books in a library. We may have a Library interface, which we call to query an underlying database. Using JMock we can write a test like this:

public class LibraryControllerTest extends MockObjectTestCase {

private static final String MODEL_NAME = "books";
private static final String INDEX_PAGE = "index.jsp";

private Mock mockLibrary;
private LibraryController controller;
private MockHttpServletRequest request;
private MockHttpServletResponse response;

protected void setUp() throws Exception {
super.setUp();
mockLibrary = mock(Library.class);

request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
request.setMethod("GET");

controller = new LibraryController();
controller.setLibrary((Library)mockLibrary.proxy());
controller.setIndexView(INDEX_PAGE);
controller.setModelName(MODEL_NAME);
}

public void testListingBooks() throws Exception {
mockLibrary.expects(once()).method("getAvailableBooks").will(returnValue(new BookList()));
ModelAndView modelAndView = controller.handleRequest(request, response);
assertThat(modelAndView.getViewName(),eq(INDEX_PAGE));
assertThat(modelAndView.getModel().get(MODEL_NAME), isA(BookList.class));
}
}

Here I have set up a mock of the Library interface, and in testListingBooks set up an expectation that this mock will have its getAvailableBooks() method called once durnig the course of the test. I have also set a stub return value, so that when this happens, the mock will return a new BookList, so that the returned value is predictable. See the JMock documentation for more on setting up mocks with expectations.

Now we have a concise test that expresses what we want our controller to do. When we request the page, we expect it to call the Library, and put that information into the ModelAndView that is returned from handleRequestInternal().
To make this test pass, again requires only a simple controller.

import org.springframework.web.servlet.mvc.AbstractController;

public class LibraryController extends AbstractController {

private String indexView;
private String modelName;
private Library library;

public void setIndexView(String viewName) { indexView = viewName; }
public String getIndexView() { return indexView; }
public void setModelName(String name) { modelName = name; }
public String getModelName() { return modelName; }
public void setLibrary(Library lib) { library = lib; }
public Library getLibrary() { return library; }

public ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
BookList list = library.getAvailableBooks();
return new ModelAndView(getIndexView(),getModelName(),list);
}
}

Another nice thing that we can do, is to test the case where there is a problem with the data source. We can set up an expectation on the mock Library that it will throw an exception when it is called, and then check that the controller gives an appropriate response, for example routing to an error page.

public class LibraryControllerTest extends MockObjectTestCase {

private static final String ERROR_PAGE = "error.jsp";
...
public void testErrorFromDataSource() throws Exception {
controller.setErrorPage(ERROR_PAGE); mockLibrary.expects(once()).method("getAvailableBooks").will(throwException(new DatabaseDownException()));
ModelAndView modelAndView = controller.handleRequest(request, response);
assertThat(modelAndView.getViewName(), eq(ERROR_PAGE));
}
}

This sets up the mock Library to throw an exception when it is called, and checks that the view that we get back is the error page we expect. We can then complete the controller.

public class LibraryController extends AbstractController {
...
// field, and getter and setter for errorPage here
...
public ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
try {
BookList list = library.getAvailableBooks();
return new ModelAndView(getIndexView(),getModelName(),list);
} catch (DatabaseDownException dde) {
return new ModelAndView(getErrorPage());
}
}
}

Again, writing the test first gives us a clear direction for what we want to do when we write the code, and we can be sure that the controller is behaving correctly when our tests pass, without having to construct any of the view components.





<< Home

This page is powered by Blogger. Isn't yours?