Slowly but surely, JConch Java Concurrency Library is becoming a depot for multithreaded and asynchronous testing on the Java platform. First there was SerialExectorService, allowing you to test with a Java ExecutorService that never started threads (example, javadoc). Then there was assertSynchronized, allowing you to make easy unit test assertions about your object's synchronization policy (javadoc, example).
And now JConch 1.2 offers TestCoordinator: a tool for testing asynchronous code and multi-threaded callbacks. The TestCoordinator solves the problem of how to properly wait for asynchronous method calls without littering your unit tests with wait/join calls or CyclicBarrier/CountDownLatch API calls. In this regard, it provides a useful unit testing abstraction over the great Java 5 concurrency primitives.
Note: If you'd like to skip all the prose then go straight to the example and unit tests that are part of JConch 1.2.
Writing good unit tests requires the same input as writing good production code: practice, dedication, and about 10,000 hours hours experience. Almost any component framework that requires some sort of event listener invariably has a unit test that looks like this:
MyComponent component = new MyComponent()
ActionEvent event
component.addActionListener({ ActionEvent e ->
event = e
} as ActionListener)
component.click()
Thread.sleep(1000)
assert event.source != null
assert "click" == event.actionCommand
OK, usually it's in Java and not Groovy, but you get the point. Life's too short to write Java at home. The simple format is 1) create a component with a listener that captures the input, 2) activate the component, and 3) put the thread to sleep for a while and hope that the event fires before your assertions get run. The obvious problem is that you end up with either slow or intermittently failing unit tests depending on how long you sleep (no, there really isn't a perfect sleep number that avoids both).In the Java 4 days this was "solved" by monkeying with Object#wait and Object#notify method calls. Ewww. Java 5 introduced CyclicBarrier and CountDownLatch which solved the problem nicely albeit in a primitive way. It is an improvement, but we can do better than this:
@Test(timeout=5000)
public void testClick() {
CyclicBarrier gate = new CyclicBarrier(2)
MyComponent component = new MyComponent()
component.addActionListener({ ActionEvent e ->
assert "click" == e.actionCommand
gate.await()
} as ActionListener)
component.click()
gate.await()
}
This approach allows us to move the assertion methods inside the callback, because we're insured that either the callback will be called or the test will fail with a timeout (the @Test(timeout=5000) bit). It is simpler and hides some complexity, but there is still a bunch of primitives distracting the reader from the core content of the test... what is that (2) parameter on the barrier constructor? Is the timeout value clear or is it hidden within a method annotation? And just what does await() mean? There is a lot of primitiveness involved here, which is what TestCoordinator abstracts over:import jconch.testing.TestCoordinator
TestCoordinator coord = new TestCoordinator()
MyComponent component = new MyComponent()
component.addActionListener({ ActionEvent e ->
assert "click" == e.actionCommand
coord.finishTest()
} as ActionListener)
component.click()
coord.delayTestFinish(1, TimeUnit.SECONDS)
As you can see, TestCoordinator acts alot like a barrier or latch, but without all the low level API. When you've activated your class under test then you call delayTestFinish(...); the unit test will wait unit the listener calls finishTest(). And if finishTest() has already been called (a frequent possibility with multithreaded systems), then delayTestFinish just proceeds. If the timeout value expires then the test fails. No funny timeout annotation. No funny API. Just a simple coordinator. You want to delay a test until a callback is finished, and the API is designed around this nomenclature.There are two other options to delay as well:
coord.delayTestFinish(1000) // milliseconds
coord.delayTestFinish() // requires @Timeout value!
For those of you coming from GWT... yes, this is almost exactly the API of GWTTestCase. Use what works, I say.For more info on TestCoordinator, check out the UnitTest and the example. Happy Testing!
4 comments:
How about using GPars DataFlow for that purpose?
It's also just like QUnit's start and stop methods. Use what works indeed.
I 2nd Dierk's comment. This looks like a perfect fit for GPars DataFlow.
Ah, I had never heard of QUnit.
Post a Comment