Here is roughly how it works (mostly for background; if that's not interesting, feel free to skip the following section):
- A
WebConnectionClientis instantiated. The client'sopenmethod is called, which executes the client'srunmethod in a background thread. - All client/server communication happens through the exchange of JSON objects (for the concrete encoding, check out the PayloadBuilder utility class). The JSON object contains an opaque key (server-provided; the server uses it to indentify the client and to find out what of its outgoing messages have been received) and a list of outgoing messages. The server responds with messages in the same format, which are passed on to a receiver class.
- New messages to the server come in through the
sendmethod Send will not automatically trigger communication, but pass the message to an internal list of outgoing data. This data will be added to the payload the next time the cyclicrunmethod communicates with the server.
Now that we know how the class is expected to behave, we'd like to verify the expected behavior. This however provides a couple of interesting challenges:
- The class uses http communication (to be precise, the Apache http client). Does that mean we need to run a web server to test the class against?
- The class uses a combination of
System.currentTimeMills()andThread.sleep()to maintain a constant frequency of the http calls. How can we make sure that it sleeps for the right time? - A method having to sleep in-between makes method execution of tests take too much time. Can we somehow speed up execution?
- How do we test execution of code that runs in its own thread?
The solution I chose was to extract every aspect that is not deterministic (time), concurrent (threading) or relies on outside systems (http calls) into an independent class called Environment:
public static interface Environment {
/**
* Performs an http request to the server
* @param data the data to be transmitted
* @return the response payload, or null if the connection failed.
*/
public String fetch(String data);
/**
* Controls the execution of the client's run-method in an independent
* thread. Similar to the Executor interface, just not for generic runnables,
* and it would also work in Java 1.3
*/
public void execute(WebConnectionClient client);
/**
* Holds the current thread for a certain amount of milliseconds
*/
public void sleep(long millis) throws InterruptedException;
/**
* Gets the current time in milliseconds
*/
public long currentTimeMillis();
}
While the real-life implementation would use system time, http client, and threading, I can easily build a mock implementation for unit tests or simply use EasyMock to automatically create such a mock on the fly.
Let's take a look at such a test setup. First, I create mocks for the two interfaces that my class under test depends on (the environment and the receiver for incoming messages):
private final WebConnectionClient.Environment environment =
EasyMock.createStrictMock(
WebConnectionClient.Environment.class);
private final WebConnectionClient.Receiver receiver =
EasyMock.createStrictMock(
WebConnectionClient.Receiver.class);
Using these mocks, I can now build a
WebConnectionClient and also gain access to its internal payload buffer (the method getQueue has package visibility, so that only our test code can access it): private final WebConnectionClient client =
new WebConnectionClient(environment, SILENCE, MAX);
private final Queue<String> queue = client.getQueue();
Now let's take a look at a concrete unit test. Say our client tries to connect to the server, but the http connect fails the first time. The client will have no messages to process, so it will go into sleep mode for a certain time. Note how (since we have control over both the system time and the sleep method) we can predict exactly how long that sleep would be. Also, note that while we verify that
sleep is called, we do not actually have to suspend the thread. We simply verify that the sleep-call took place, then simulate an end-of-execution situation by throwing an InterruptedException: public void testSimpleLoop() throws Exception {
// Record a very simple sequence for the environment:
// The first http-send will fail, which will cause the
// thread to sleep. We will jump out of the loop by
// throwing an InterruptedException
EasyMock.expect(environment.currentTimeMillis()).andReturn(0L);
EasyMock.expect(environment.fetch("{\"payload\":[]}")).andReturn(null);
EasyMock.expect(environment.currentTimeMillis()).andReturn((long) (SILENCE - 1));
environment.sleep(1L);
EasyMock.expectLastCall().andThrow(new InterruptedException("end of test"));
// Now, let's try this out
EasyMock.replay(environment, receiver);
try {
client.run();
fail("Expected an InterruptedException");
} catch (InterruptedException expected) {
// fall through
}
EasyMock.verify(environment, receiver);
}
Suddenly, testing complex client-server interaction with threading has become very, very simple :-). Let's look at a more complex example. For readability, I have replaced the JSON payload strings with either constants or a simple generation method
p(String meta, String... messages). For details, check out the complete source file.public void testSendUntilQueueIsEmpty() throws Exception {
// Push a few (MAX + 1) messages onto the bus
for (int i = 0; i <= MAX; i++) {
client.send("" + (i + 1));
}
// First, a connection is established
EasyMock.expect(environment.currentTimeMillis()).andReturn(0L);
EasyMock.expect(environment.fetch(EMPTY)).andReturn(META_A);
EasyMock.expect(environment.currentTimeMillis()).andReturn((long) (SILENCE + 1));
// Next, the first three messages get delivered
EasyMock.expect(environment.currentTimeMillis()).andReturn(0L);
EasyMock.expect(environment.fetch(p("a", "1", "2", "3"))).andReturn(META_B);
EasyMock.expect(environment.currentTimeMillis()).andReturn((long) (SILENCE + 1));
// Then, the fourth message gets delivered
EasyMock.expect(environment.currentTimeMillis()).andReturn(0L);
EasyMock.expect(environment.fetch(p("b", "4"))).andReturn(META_C);
EasyMock.expect(environment.currentTimeMillis()).andReturn((long) (SILENCE + 1));
// Now, the queue should be empty.
// An empty payload gets delivered, just to poll for new messages
// from the server.
EasyMock.expect(environment.currentTimeMillis()).andReturn(0L);
EasyMock.expect(environment.fetch(META_C)).andReturn(META_D);
EasyMock.expect(environment.currentTimeMillis()).andReturn((long) (SILENCE - 1));
environment.sleep(1L);
// Let's try it out
EasyMock.expectLastCall().andThrow(new InterruptedException("end of test"));
EasyMock.replay(environment, receiver);
try {
client.run();
fail("Expected an InterruptedException");
} catch (InterruptedException expected) {
// fall through
}
EasyMock.verify(environment, receiver);
}
This test recorded a relatively complex sequence of client/server interactions. It also controls whether and for how long the client sleeps by setting the elapsed time between two calls either to
SILENCE + 1 (one millisecond longer than the interval, so no sleep) or SILENCE - 1 (one millisecond short, so we have to sleep).This approach also works for many other things that are outside the control of a class under test, such as random numbers or data from the file system (btw: check out Google App Engine Virtual File System project!). Any good use case I forgot to mention? Put it into a comment on this post :-)
0 comments:
Post a Comment