|
|
|||||||||||||||||||||||||||
by Bob McCune | |||||||||||||||||||||||||||
| |||||||||
Unit testing is an essential practice for anyone seeking to develop better-designed, higher quality software. Testing the individual objects that make up your system provides you with a much higher degree of confidence in both the design and general quality of the application. However, testing objects in isolation often presents some unique challenges as few useful objects operate independently of others. The challenges are even greater in the context of a J2EE application where the container manages many of the collaborating objects. This article will look at the use of mock objects and the Mockrunner testing framework as a means of overcoming many of these challenges.
The topic of mock objects is often a point of confusion for those new to test-driven development, so before diving into the details of Mockrunner, it would be best to make sure you have an understanding of what exactly is a mock object.
A mock object, also referred to simply as a mock, plays the role of a stand-in for a real application object in the context of a unit test. A key objective in unit testing is to ensure you are testing only the functionality of your class under test and not that of its collaborating objects. Mocks help you to achieve this goal by providing a replacement object, which implements the same interface, but contains little or no state or behavior. This allows you to focus solely on the logic of the class you are testing without concern for the impacts of the objects with which it interacts. I'll refer you to the resources section for links to further insights and perspectives on the topic.
Mockrunner is a lightweight testing framework, built on JUnit, for testing J2EE applications. Its focus is on transparently simulating your application's runtime environment so you can easily create unit tests that run out-of-container and independently of deployment descriptors or other external artifacts.
The core distribution provides built-in support for testing the most commons J2EE component types including the Servlet APIs, JDBC, JMS, EJB, as well as Struts Actions. Its comprehensive API support makes Mockrunner a compelling tool as it provides a consistent framework for testing your applications from end to end.
There are three primary categories of classes found in the Mockrunner framework: Test Modules, TestCase Adapters, and Mock Object Factories. These categories do not necessarily denote a particular object hierarchy, but rather represent a conceptual grouping. The following table describes the purpose of each category and lists some examples of the corresponding classes found in the distribution.
| Type | Purpose | Examples |
|---|---|---|
| Test Modules | Provides the central runtime behavior of the framework. | |
| TestCase Adapters | Extensions of JUnit's TestCase that
act as wrappers around the functionality of their underlying test
modules. |
|
| Mock Object Factories | Factory objects for creating the various types of mocks required by a particular J2EE component type. | |
The diagram in Figure 1 shows a high-level view of how the classes in the various categories relate.

Figure 1. Core MockRunner Relationships
At the heart of the framework are the various test modules. The test modules provide the runtime testing behavior and are used to mimic the functionality of the J2EE container. Although you can interact directly with the test modules, which may be necessary if you already have a standard base class from which your tests extend, it is often more convenient to have your test cases extend from one the framework's TestCase adapters.
The TestCase adapters are extensions of JUnit's
TestCase. They provide a standard implementation of
the Adapter pattern and are used to wrap the functionality of the
underlying test modules and related mock object factories. Under
each technology type you will find two different versions of the
adapters:
Basic<Technology>TestCaseAdapter contains a reference to the technology-specific test module and
related mock object factory. Examples in the distribution include BasicServletTestCaseAdapter and
BasicEJBTestCaseAdapter.
<Technology>TestCaseAdapter contains
a reference to all of the technology test modules and mock object
factories defined in the Mockrunner distribution. Examples include
the ActionTestCaseAdapter and
JMSTestCaseAdapter.
You may find the non-basic versions to be useful in situations where you are testing a class that mixes multiple J2EE technologies. For instance, if you are testing a servlet class that directly looks up and interacts with an EJB, the non-basic version may be useful to you. Assuming your application has an adequate level of abstraction, you will likely find the basic versions more generally applicable and will be the ones on which I will focus in this article.
Let's begin by looking at some specific examples.
The Struts framework is
often the key technology used in an application's web tier. Given
its role in an application it is important for you to provide a
sufficient level of test coverage to ensure the quality of this
tier. Unfortunately, testing Struts Actions can be difficult due to
their dependence on objects managed by the web container and the
Struts infrastructure itself. Luckily, Mockrunner provides you with
the tools to easily create tests that run out-of-container and
independently of any external configuration. Let's begin by looking
at a simple Action class used to search an online
catalog.
public class SearchAction extends Action {
public ActionForward execute(ActionMapping mapping,
ActionForm actionForm,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
SearchForm form = (SearchForm) actionForm;
String query = form.getQuery();
SearchService service = getSearchService();
List list = service.searchCatalog(query);
if (list != null && !list.isEmpty()) {
request.setAttribute("results", list);
} else {
ActionMessages messages = new ActionMessages();
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("msg.no.results"));
saveMessages(request, messages);
return mapping.getInputForward();
}
return mapping.findForward(ForwardKeys.SUCCESS);
}
SearchService getSearchService() {
ServletContext context =
getServlet().getServletContext();
return (SearchService)
context.getAttribute("searchService");
}
}
The SearchAction retrieves the user input from a
subclass of ActionForm to determine the user's query.
It then invokes the business logic tier's
searchCatalog() method and depending on the results
returned from the service, the action will either populate the
request with the list of results or an ActionMessage
indicating no results were found.
You'll begin by creating a subclass of
BasicActionTestCaseAdapter. This class provides a
wrapper around the ActionTestModule and
ActionMockObjectFactory, which provide the
functionality required for testing Struts Actions. The
SearchAction interacts with a business logic tier
object bound the ServletContext, so you'll first need
to configure a mock version of this class and bind it to the
framework's MockServletContext. This example will use
EasyMock to create a mock
version the SearchService. Please refer back to Lu
Jian's Mock Objects in Unit Tests for the details of its syntax. The
setUp() method for this test case is defined as
follows:
public class SearchActionTest
extends BasicActionTestCaseAdapter {
private MockControl ctrl;
private SearchService service;
private final String query = "Test Query";
protected void setUp() throws Exception {
super.setUp();
// Configure EasyMock-managed mock service
ctrl =
MockControl.createControl(
SearchService.class);
service = (SearchService) ctrl.getMock();
ActionMockObjectFactory factory =
getActionMockObjectFactory();
MockServletContext context =
factory.getMockServletContext();
// bind mock service to the servlet context
context.setAttribute(
ServiceKeys.SEARCH_SERVICE, service);
}
...
}
It is important to note if you override an adapter's
setUp() or tearDown() method that you
always invoke the super class version to ensure proper
initialization or clean up of the test case.
With the setUp() configuration complete you can
proceed to test the action's execute() method. In this
example, you can assume the user has input a valid search string,
which leaves two primary test scenarios:
A valid list of results was returned and bound to the request and the request was forwarded to the results page.
No results were found and the user was returned to the input page with an ActionMessage bound to the request indicating the status of the query.
Let's begin with the first scenario.
public void testExecute() {
List expected = new ArrayList();
expected.add("foo");
// configure expected method call
// and return value
service.searchCatalog(query)
ctrl.setReturnValue(expected);
ctrl.replay();
// run the action's execute method
SearchForm form = createForm(query);
actionPerform(SearchAction.class, form);
// verify the results of the action processing
verifyNoActionErrors();
verifyNoActionMessages();
verifyForward(ForwardKeys.SUCCESS);
List results =
(List) getRequestAttribute("results");
assertEquals(expected, results);
ctrl.verify();
}
private SearchForm createForm(String query) {
SearchForm form = new SearchForm();
form.setQuery(query);
return form;
}
The mock SearchService is configured to expect its
searchCatalog() method be invoked and return a
List containing one result. For the purposes of the
test, the actual contents of the list are irrelevant. Next, the
actionPerform() method is called which sets the
runtime into motion and invokes the action's execute()
method. After the execute() method has been run, you
can use the various verifyXXX() methods
provided by BaseActionTestCaseAdapter to verify the
test results. Specifically, a check is made to verify no
ActionErrors or ActionMessages were
created and that the expected forward occurred. Finally, a quick
examination of the HttpServletRequest is performed to
ensure it was populated with the results of the query.
Mockrunner requires no external configuration, so the first test method is complete and ready to be run. You can use any standard approach to executing this test including using JUnit's TestRunner, Ant, or your IDE's built-in JUnit support.
With the first test complete, it's time to move on to testing the second scenario.
public void testExecuteNoResults() {
ctrl.expectAndReturn(service.searchCatalog(query),
Collections.EMPTY_LIST);
ctrl.replay();
getMockActionMapping().setInput("/testInput");
actionPerform(SearchAction.class, createForm(query));
verifyNoActionErrors();
verifyActionMessagePresent(MsgKeys.MSG_NO_RESULTS);
verifyForward(getMockActionMapping().getInput());
ctrl.verify();
}
As in the previous test example, the first step is to configure
the expectations of the SearchService. This time it
needs to be configured to return an empty list indicating no
results were found. The next step is to configure the action's
input forward by using the setInput() method of the
MockActionMapping class. Once again, a call to invoke
the actionPerform() method is made to run the action's
execute() method. This time a verification check is
made to ensure an ActionMessage was bound to the
request and the user was redirected to the input page.
You now have a complete out-of-container test case for the
SearchAction.
Many of the applications I've developed over the past couple of
years have used a combination of Struts and the Spring Framework. A common
approach when using these two technologies together is to have your
Actions lookup bean references from the Spring
WebApplicationContext. Obtaining a reference to this
class can be done either by subclassing one of Spring's Action
extensions or by using its WebApplicationContextUtils
class. The core Mockrunner distribution does not provide support
for Spring, however, this capability can be easily added by writing
a simple extension to the framework.
The first thing you need to create is a mock implementation of
Spring's WebApplicationContext. This will allow you to
test your actions without the need to load the Spring container or
rely on bean definitions wired in Spring's
applicationContext.xml. Since
WebApplicationContext is an interface, it is easy to
create a stubbed version of this class and add the minimal amount
of functionality needed to run a test. Let's look at an excerpt
from this class showing the key methods.
public class MockWebApplicationContext
implements WebApplicationContext {
private long startup;
private ServletContext servletContext;
private Map beanMap =
Collections.synchronizedMap(new HashMap());
public MockWebApplicationContext(ServletContext
servletContext) {
this.servletContext = servletContext;
startup = Calendar.getInstance().getTimeInMillis();
}
public ServletContext getServletContext() {
return servletContext;
}
public void addBean(String beanName, Object bean) {
beanMap.put(beanName, bean);
}
public Object removeBean(String beanName) {
return beanMap.remove(beanName);
}
public Object getBean(String beanName)
throws BeansException {
return beanMap.get(beanName);
}
...
}
With the MockWebApplicationContext complete, the
next step is to create a simple extension of the
BasicActionTestCaseAdapter to incorporate this
mock.
public abstract class
BasicSpringActionTestCaseAdapter
extends BasicActionTestCaseAdapter {
private MockWebApplicationContext wac;
/**
* Configure the MockWebApplicationContext and
* set it as an attribute on the ServletContext.
*
* @throws Exception
*/
protected void setUp() throws Exception {
super.setUp(); // ensure super initialized
ActionMockObjectFactory factory =
getActionMockObjectFactory();
ServletContext sc =
factory.getMockServletContext();
wac = new MockWebApplicationContext(sc);
sc.setAttribute(
WebApplicationContext.
ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
wac
);
}
protected MockWebApplicationContext
getMockWebApplicationContext() {
return wac;
}
}
To show this extension in action I'll refactor the earlier example's implementation class and test case.
Refactored getSearchService() method from SearchAction
SearchService getSearchService() {
ServletContext sc = getServlet().getServletContext();
WebApplicationContext wac =
WebApplicationContextUtils.getWebApplicationContext(sc);
return (SearchService) wac.getBean("searchService");
}
Refactored setUp() method from SearchActionTest
public class SearchActionTest
extends BasicSpringActionTestCaseAdapter {
private SearchService service;
private MockControl serviceCtrl;
protected void setUp() throws Exception {
super.setUp();
serviceCtrl =
MockControl.createControl(SearchService.class);
service =
(SearchService) serviceCtrl.getMock();
MockWebApplicationContext wac =
getMockWebApplicationContext();
wac.addBean("searchService", service);
}
...
}
Now that the web tier has been covered, let's move on to another key component of an enterprise application, the data access tier.
Most developers involved in enterprise Java development are quite familiar with writing JDBC code to access a relational database. It's a simple and easy to use API, but can be a source of serious application problems if not written correctly. Given the importance of the data access tier you'll want to apply the same testing rigor as you would elsewhere in your application.
Testing a data access component in isolation generally means
testing it independently of a relational database. Although
performing integration tests with the real database is important,
ideally, you'll want to begin your testing efforts by focusing on
the logic of your DAO and not its interaction with an external data source.
Mockrunner, once again, provides you with the tools to create
isolated tests with a minimal amount of coding and configuration.
Let's look at a simple JDBC-based DAO implementation used to lookup
a User object and see how Mockrunner can be used to
test this class.
public class UserDaoImpl implements UserDao {
private final static String SELECT_USER =
"SELECT * FROM USER WHERE USER.USERNAME = ?";
public User getUser(String username)
throws DaoException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = getDataSource().getConnection();
ps = conn.prepareStatement(SELECT_USER);
ps.setString(1, username);
rs = ps.executeQuery();
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getInt("ID"));
user.setUsername(rs.getString("USERNAME"));
user.setFirstname(rs.getString("FIRSTNAME"));
user.setLastname(rs.getString("LASTNAME"));
user.setEmail(rs.getString("EMAIL"));
}
return user;
} catch (Exception e) {
String msg = "The user could not be retrieved.";
throw new DaoException(msg, e);
} finally {
try {
if (rs != null) {
rs.close();
}
if (ps != null) {
ps.close();
}
if (conn != null && !conn.isClosed()) {
conn.close();
}
} catch (SQLException sqle) {
String msg = "Could not close resources.";
throw new DaoException(msg, sqle);
}
}
}
private DataSource getDataSource() throws Exception {
InitialContext context = new InitialContext();
return (DataSource) context.lookup("jdbc/ExampleDS");
}
}
Despite the simplicity of the example, it still illustrates
several concerns common to most JDBC code. In particular, you'll
want to ensure the correct SQL is being executed, the data returned
from the query is being properly mapped into the domain object, and
you'll want to ensure all JDBC resources are being properly
released. Begin by creating a new test case extending from
BasicJDBCTestCaseAdapter. This adapter class, like the
test case adapter used with actions, provides a wrapper around the
underlying test module and its related mock object factory -- in
this case JDBCTestModule and
JDBCMockObjectFactory, respectively. The DAO is
looking up a DataSource bound to a JNDI context, so
you'll begin by adding this binding in the setUp()
method.
public class UserDaoJdbcTest
extends BasicJDBCTestCaseAdapter {
private UserDaoJdbc dao;
protected void setUp() throws Exception {
super.setUp();
JDBCMockObjectFactory factory =
getJDBCMockObjectFactory();
MockDataSource ds = factory.getMockDataSource();
dao = new UserDaoJdbc();
MockContextFactory.setAsInitial();
InitialContext context = new InitialContext();
context.rebind("jdbc/ExampleDS", ds);
}
protected void tearDown() throws Exception {
super.tearDown();
MockContextFactory.revertSetAsInitial();
}
...
}
A MockDataSource reference is obtained from the
JDBCMockObjectFactory class and is bound to an
InitialContext. The call to
MockContextFactory.setAsInitial() configures the
factory as the JNDI provider for the test. With the test fixture
configured you can move on to test the getUser()
method.
public void testGetUser() {
createResultSet();
User user = dao.getUser("foobar");
String sql =
"SELECT * FROM USER WHERE USER.USERNAME = ?";
verifySQLStatementExecuted(sql);
verifyAllResultSetsClosed();
verifyAllStatementsClosed();
verifyConnectionClosed();
// verify that all values have been properly
// mapped to domain object
assertEquals(1, user.getId());
assertEquals("foobar", user.getUsername());
assertEquals("Foo", user.getFirstname());
assertEquals("Bar", user.getLastname());
assertEquals("foo@bar.com", user.getEmail());
}
private void createResultSet() {
PreparedStatementResultSetHandler handler
= getPreparedStatementResultSetHandler();
MockResultSet rs = handler.createResultSet();
rs.addColumn("ID", new Object[]{"1"});
rs.addColumn("USERNAME", new Object[]{"foobar"});
rs.addColumn("FIRSTNAME", new Object[]{"Foo"});
rs.addColumn("LASTNAME", new Object[]{"Bar"});
rs.addColumn("EMAIL", new Object[]{"foo@bar.com"});
handler.prepareGlobalResultSet(rs);
}
The first line of code in the test is a call to the private
createResultSet() method. This method creates an
instance of MockResultSet which will be returned when
the executeQuery() method is called on the
PreparedStatement. Mockrunner allows you to bind this
ResultSet either locally, meaning to a specific SQL
query, or globally so it will be the ResultSet
returned regardless of the query executed. Since there is only a
single SQL query to be executed, the global binding is
sufficient.
The getUser() method is invoked and its results are
verified. Specifically, it is important to verify the correct SQL
statement was executed and that all of the JDBC resources have been
properly closed and released. Finally, a quick check is made to
ensure the values contained in the ResultSet have been
properly mapped into the User object.
You now have a complete unit test to exercise the DAO logic that can be run without the need for a container or database.
Hopefully this article has provided you with a useful introduction to the Mockrunner framework. Although I've provided several common usage examples, this article has only shown only a small subset of the complete capabilities of the framework. Thankfully, the core distribution ships with several useful examples of technologies not covered in this introduction as well as some further approaches to the topics already addressed. I think you'll find Mockrunner to be a useful addition to your test-driven development toolbox.
Bob McCune is a General Partner with Twin Cities-based OpenPrinciple Consulting.
View all java.net Articles.
|
|