First, a little background. In the ISRL lab, we have a Java servlet that is being used to generate security keys for users. I have been tasked with writing unit tests for this servlet. The tests are such that we want to test the raw performance of the servlet’s actual work (i.e., not including network latency and ancillary issues). So, I’m writing a bunch of JUnit tests that execute the servlet outside of the context of a servlet container (like Tomcat, Jetty, etc).
Servlet containers provide servlets with a number of support objects that, during my tests, would be missing. These are objects like HttpServletRequest, HttpServletResponse, ServletConfig, ServletContext, and ServletInputStream. Sounds like a job for the Mock Object pattern.
After looking around a bit, I came across EasyMock. This is what the web site has to say:
EasyMock provides Mock Objects for interfaces in JUnit tests by generating them on the fly using Java's proxy mechanism. Due to EasyMock's unique style of recording expectations, most refactorings will not affect the Mock Objects. So EasyMock is a perfect fit for Test-Driven Development.
EasyMock allows you to programmatically generate mock objects from Java interfaces, add behavior to them, and monitor that behavior. For example, suppose we wanted an HttpServletRequest mock object, and we wanted to specify the behavior of its getContentType() method:
1: HttpServletRequest mock = EasyMock.createMock(HttpServletRequest.class);
2:
3: // add getContentType() behavior
4: EasyMock.expect(mock.getContentType()).andReturn("text/html").atLeastOnce();
5:
6: EasyMock.replay(mock);
So, in the snippet above, we create a mock object by calling the static createMock() method on the EasyMock class. That method takes one parameter of type Class and returns an object that implements the HttpServletRequest interface. At this stage, all of the mock object’s non-void methods return null or some default value. To get the getContentType() method to return text/html, we call the expect() method on the EasyMock class, passing it mock.getContentType(), which gives us an expectation setter, on which we can call the andReturn() and atLeastOnce() methods. This means that calls to mock.getContentType() should always return what was passed into the andReturn() method, and that the mock.getContentType() method should be called at least once. The call to EasyMock.replay() registers the above behavior. So, after the code above, whenever I call mock.getContentType(), I’ll get text/html. Note that I didn’t have to implement the HttpServletRequest interface myself.
Now, suppose I wanted to simulate dynamic behavior, as I might with an InputStream object, for example. That is, I might want this object’s read() method to return different values at different times, as if the object were an actual InputStream of data. I had to do this to simulate ServletInputStream. EasyMock can handle this as well. Here is what you can do:
1: public static ServletInputStream createMockServletInputStream(String input)
2: throws IOException {
3:
4: ServletInputStream mock = createMock(ServletInputStream.class);
5:
6: MockInputStream fakeInputStream = new MockInputStream(input);
7:
8: // add read()
9: EasyMock.expect(mock.read()).andAnswer(
10: new InputStreamReadAnswer(fakeInputStream)
11: ).atLeastOnce();
12:
13: // add read(byte[], int, int)
14: EasyMock.expect(mock.read(
15: EasyMock.capture(new Capture()),
16: EasyMock.capture(new Capture()),
17: EasyMock.capture(new Capture())
18: )
19: ).andAnswer(new InputStreamReadBytesAnswer(fakeInputStream)).atLeastOnce();
20:
21: // add close()
22: mock.close();
23:
24: EasyMock.replay(mock);
25:
26: return mock;
27: }
So, in this example, on line 6, you see that I had to implement a MockInputStream, which simply extends the InputStream class. Here it is:
1: import java.io.InputStream;
2:
3: public class MockInputStream extends InputStream {
4: private String input;
5: private int pos;
6:
7: private final int EOF = -1;
8:
9: /**
10: * Creates a fake InputStream with the specified input.
11: * @param input the data which the InputStream works on
12: */
13:
14: public MockInputStream(String input) {
15: this.input = input;
16: this.pos = 0;
17: }
18:
19: /**
20: * @see #readBytes(byte[], int, int)
21: */
22: public int readBytes(byte[] bytes, int offset, int length) {
23: if (length == 0)
24: return 0;
25: else if (pos >= input.length())
26: return EOF;
27: else {
28: int bytesRead = 0;
29: byte singleByte = 0;
30:
31: for (int i = offset; i < offset + length; ++i) {
32: singleByte = (byte) read();
33: if (singleByte != EOF) {
34: bytes[i] = singleByte;
35: bytesRead++;
36: }
37: else break;
38: }
39:
40: return bytesRead;
41: }
42: }
43:
44: /**
45: * @see #read()
46: */
47: public int read() {
48: int read;
49:
50: if (pos < input.length())
51: read = (int) input.charAt(pos++);
52: else
53: read = EOF;
54:
55: return read;
56: }
57: }
As you can see, it takes a String and operates on it as if it were a data stream. Now, take a look at line 9 of the createMockServletInputStream() method above. We call the expect() method as before, but this time we call andAnswer() on the returned expectation setter. We pass it a newly-created object of type InputStreamReadAnswer, which takes the MockInputStream in its constructor. The InputStreamReadAnswer object implements the EasyMock IAnswer interface, and its job is to simulate calls to the read() method on the mock ServletInputStream object. EasyMock looks for an object that implements this interface and calls its answer() method. Here’s the InputStreamReadAnswer object:
1: import org.easymock.IAnswer;
2:
3: public class InputStreamReadAnswer implements IAnswer {
4: private MockInputStream fake;
5:
6: /**
7: * Creates an InputStreamReadAnswer
8: * @param fake a MockInputStream
9: */
10: public InputStreamReadAnswer(MockInputStream fake) {
11: this.fake = fake;
12: }
13:
14: /**
15: * Simulates the behavior of calls to InputStream.read().
16: * @return an int that corresponds to the current character in the stream
17: * @throws Throwable
18: * @see InputStream.read()
19: */
20: @Override
21: public Integer answer() throws Throwable {
22: return fake.read();
23: }
24: }
It’s quite simple, as you can see. The answer() method simply calls the read() method on the MockInputStream.
Now, suppose that you want to simulate a method that takes arguments. Look at line 13 of createMockServletInputStream(). Here we implement the read(byte[], int, int) method of InputStream. Note that, for each argument, I pass in new Capture objects, parameterized with the type of each argument expected by read(byte[], int, int), surrounded by calls to EasyMock.capture(). This enables EasyMock to capture the values of these arguments and handle them in the appropriate IAnswer object, which, in this case, is an InputStreamReadBytesAnswer object that I created:
1: import org.easymock.classextension.*;
2:
3: import org.easymock.IAnswer;
4:
5: public class InputStreamReadBytesAnswer implements IAnswer {
6: private MockInputStream fake;
7:
8: /**
9: * Constructs an InputStreamReadBytesAnswer.
10: * @param fake a MockInputStream
11: */
12: public InputStreamReadBytesAnswer(MockInputStream fake) {
13: this.fake = fake;
14: }
15:
16: /**
17: * Simulates the behavior of InputStream.readBytes(byte[], int, int).
18: * @return an int indicating the number of bytes read
19: * @throws Throwable
20: * @see InputStream.readBytes()
21: */
22: @Override
23: public Integer answer() throws Throwable {
24: Object[] args = EasyMock.getCurrentArguments();
25:
26: byte[] bytes = (byte[]) args[0];
27: int offset = (Integer) args[1];
28: int length = (Integer) args[2];
29:
30: return fake.readBytes(bytes, offset, length);
31: }
32: }
In the answer() method above, I get the values of those captured arguments by calling EasyMock.getCurrentArguments(). Then I pass those arguments to the MockInputStream object I already created.
EasyMock makes unit testing easier by cutting down on the amount of code you have to write when using mock objects in unit testing. I have historically not had much of an inclination for unit testing, even though I recognize that I should be more inclined to it. EasyMock makes it a lot easier.