http://blog.frankel.ch/two-different-mocking-approaches
Whether you choose to choose to adopt TDD or a more traditional approach, you will test your classes in isolation. This usually mean you code with interfaces and inject your dependencies in your classes.
Last year, I had to test a servlet. Servlets are the showcase to demonstrate out-of-container testing because of their dependencies to said container in their doXxx()
method. Every one of these methods has a dependency on HttpServletRequest
and HttpServletResponse
. Both are interfaces that have no concrete implementations in the Servlet API. So, basically, in your tests, you’re stuck with three options:
- Use your container implementations (yuck!),
- Create your own implementations (rather time-consuming),
- Use already available implementations that have no dependencies.
Choosing option the 3rd option, I found this little jewel, MockRunner. MockRunner put at your dispositions mock implementations for the following API and frameworks:
- JNDI
- EJB2,
- JDBC,
- JMS,
- Servlet,
- TagLib,
- Struts.
All these mock implementations run as they should. For example, if you put an object into a mock session at the start of your test, and if you’re still in the same request afterwards, you can check the object is still here. Let’s consider the following servlet method to test:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override protected void doGet(HttpServletRequest aRequest, HttpServletResponse aResponse) throws ServletException,IOException { String action = aRequest.getParameter("action"); HttpSession session = aRequest.getSession(); Object object = session.getAttribute("number"); int number = object == null ? 0 : (Integer) object; if ("add".equals(action)) { number++; session.setAttribute("number", number); } else if ("remove".equals(action)) { number--; session.setAttribute("number", number); } else if ("reset".equals(action)) { session.setAttribute("number", 0); } }
How do you test this code with MockRunner? The first thing is to inherit from
com.mockrunner.servlet.BasicServletTestCaseAdapter
. Code follows:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 public
class
MockRunnerTest
extends
BasicServletTestCaseAdapter {
/**
* Setup the servlet to test.
*/
@Override
public
void
setUp()
throws
Exception {
super
.setUp();
createServlet(SessionServlet.
class
);
}
/**
* Test method for
* {@link SessionServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}.
*
* @throws IOException
* @throws ServletException
*/
public
void
testDoGetAdd()
throws
ServletException, IOException {
addRequestParameter(
"action"
,
"add"
);
doGet();
Object object = getSessionAttribute(
"number"
);
assertTrue(Integer.
class
.isAssignableFrom(object.getClass()));
int
number = (Integer) object;
assertEquals(
1
, number);
}
/**
* Test method for
* {@link SessionServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}.
*
* @throws IOException
* @throws ServletException
*/
public
void
testDoGetRemove()
throws
ServletException, IOException {
addRequestParameter(
"action"
,
"remove"
);
doGet();
Object object = getSessionAttribute(
"number"
);
assertTrue(Integer.
class
.isAssignableFrom(object.getClass()));
int
number = (Integer) object;
assertEquals(-
1
, number);
}
/**
* Test method for
* {@link SessionServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}.
*
* @throws IOException
* @throws ServletException
*/
public
void
testDoGetReset()
throws
ServletException, IOException {
addRequestParameter(
"action"
,
"reset"
);
doGet();
Object object = getSessionAttribute(
"number"
);
assertTrue(Integer.
class
.isAssignableFrom(object.getClass()));
int
number = (Integer) object;
assertEquals(
0
, number);
}
}
Testing our servlet is fine like this although using MockRunner has several limitations:
- you have to inherit from MockRunner’s base classes,
- thus, you’re stuck with using JUnit v3. You cannot use TestNG or JUnit v4 either,
- you are limited in what you can test out-of-the-box. If, for a reason or another, you decided to create a super servlet with additional functionalities, you are condemned to extend the MockRunner framework. Similarly, if a new API emerge, you have to create your own adapters,
- you have to remember to call
super.setUp()
in yoursetUp()
method (really not a good idea), - last, but not least, MockRunner project has seen no activity since summer ’08.
Then I got interested in Mockito. Mockito is a “true” Mock framework that enhances your interfaces and you classes with CGLIB to provide stubs for methods: that is you provide code that is chained to method invocation. Mockito seems to be very hype nowadays, but the trend is still catching. The following code shows you a test class made with Mockito:
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 public class MockitoTest {/** Servlet under test. */ private SessionServlet servlet; /** Mock request. */ private HttpServletRequest request; /** Mock response. */ private HttpServletResponse response; /** Mock session. */ private HttpSession session; /** Session's attribute map. */ private Map attributes; /** Request's parameter map. */ private Map parameters; /** * Launches Mockito configuration from annotations. */ @Before public void setUp() { attributes = new HashMap(); parameters = new HashMap(); servlet = new SessionServlet(); request = mock(HttpServletRequest.class); response = mock(HttpServletResponse.class); session = mock(HttpSession.class); when(request.getSession()).thenReturn(session); when(request.getParameterMap()).thenReturn(parameters); when(request.getParameter(anyString())).thenAnswer(new Answer() { /** * @see org.mockito.stubbing.Answer#answer(org.mockito.invocation.InvocationOnMock) */ @Override public Object answer(InvocationOnMock aInvocation) throws Throwable { String key = (String) aInvocation.getArguments()[0]; return parameters.get(key); } }); when(session.getAttribute(anyString())).thenAnswer(new Answer() { /** * @see org.mockito.stubbing.Answer#answer(org.mockito.invocation.InvocationOnMock) */ @Override public Object answer(InvocationOnMock aInvocation) throws Throwable { String key = (String) aInvocation.getArguments()[0]; return attributes.get(key); } }); Mockito.doAnswer(new Answer() { /** * @see org.mockito.stubbing.Answer#answer(org.mockito.invocation.InvocationOnMock) */ @Override public Object answer(InvocationOnMock aInvocation) throws Throwable { String key = (String) aInvocation.getArguments()[0]; Object value = aInvocation.getArguments()[1]; attributes.put(key, value); return null; } }).when(session).setAttribute(anyString(), anyObject()); } /** * Test method for * {@link SessionServlet#doGet(HttpServletRequest, HttpServletResponse)} . * * @throws IOException * @throws ServletException */ @Test public void testDoGetAdd() throws ServletException, IOException { parameters.put("action", "add"); servlet.doGet(request, response); Object object = attributes.get("number"); assertNotNull(object); assertTrue(Integer.class.isAssignableFrom(object.getClass())); int number = (Integer) object; assertEquals(1, number); } /** * Test method for * {@link SessionServlet#doGet(HttpServletRequest, HttpServletResponse)} . * * @throws IOException * @throws ServletException */ @Test public void testDoGetRemove() throws ServletException, IOException { parameters.put("action", "remove"); servlet.doGet(request, response); Object object = attributes.get("number"); assertNotNull(object); assertTrue(Integer.class.isAssignableFrom(object.getClass())); int number = (Integer) object; assertEquals(-1, number); } /** * Test method for * {@link SessionServlet#doGet(HttpServletRequest, HttpServletResponse)} . * * @throws IOException * @throws ServletException */ @Test public void testDoGetReset() throws ServletException, IOException { parameters.put("action", "reset"); servlet.doGet(request, response); Object object = attributes.get("number"); assertNotNull(object); assertTrue(Integer.class.isAssignableFrom(object.getClass())); int number = (Integer) object; assertEquals(0, number); } }
Mockito suffers from a severe lack of documentation for newbies. Though the Mockito class is well documented, I think an external presentation about the philosophy and the architecture would have been in order. This is a limitation I could adress to many Google Code project, though.
IMHO, MockRunner is more integrated with the API, hiding implementation details whereas with Mockito, you need to know about the implementation in order to provide adequate stubs. Mockito’s test class is about twice the code size than MockRunner’s. Thus, I would keep using MockRunner for classes using their integrated API, though it would force me to use an older version of JUnit, and use Mockito where there’s no such constraint.
In conclusion, I would admit both these testing harness have very different approaches and very different scopes. I find Mockito a bit complex to use, still.
To make your own mind: