Testing Asynchronous Code with GPars Dataflows
In my last post I showed how to use JConch 1.2
to unit test asynchronous code. It contains a locking/barrier mechanism
that allows you to gracefully tell your unit test how to wait and
proceed for off thread events without sleeping or polling. Whee!
As
a small example, here is how the TestCoordinator would be used to test
a mouse click event that happens on a different thread:
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)
If you are living in a Groovy world, then you should consider using GPars Dataflow variables instead of the JConch TestCoordinator. The advantage of using Dataflows is that you get a similar thread coordination API but also get a variable capture API, allowing you to move assertion statements to the end of your unit test where they belong. If you are looking for a clean, minimalist approach to capturing off thread events, then dataflows might be exactly what you need:
import java.awt.event.*
import javax.swing.SwingUtilities
import groovyx.gpars.dataflow.DataFlowVariable
def event = new DataFlowVariable()
MyComponent component = new MyComponent()
component.addActionListener({ ActionEvent e ->
event << e
} as ActionListener)
component.click()
ActionEvent e = event.val
assert e.source != null
assert e.actionCommand == "click"
A DataFlowVariable is a little like a Java Future
mixed with an immutable, thread safe object. The DataFlowVariable has
an underlying value which can be set once, and only once, and trying to
access that value will block the caller until the value is available.
In this example, setting the value is done in the event handler with
the "event << e" line, and accessing the value is done in the
"event.val" line. Getting the "val" will block the caller until a value
is available. (Remember, in Groovy .val property access is translated
into a getVal() method call).
The advantage of using Dataflows
in testing, when compared to Java, is that you get a nicer thread
synchronization API than what the java.util.concurrent primitives
provide. The advantage of Dataflows, when compared to JConch, is that
you get to move your assertion methods back to the end of the unit
tests. William Wake described a format for unit tests called Arrange-Act-Assert,
and it is still one of the best guidelines to writing clear and
understandable unit tests. Assertion methods belong at the end of your
test and arrangement code belongs at the beginning (note how many mock
object frameworks subvert this ordering!). Capturing a variable is a
good way to move the assertions to the end of the method, but in most
frameworks it requires copious amounts of accidentally complex code.
The
downside of using Dataflows is that you once again have to set an
@Timeout value on your unit test in the event that you never get a
value (otherwise your tests will hang). OH WAIT, that is totally wrong.
The GPars authors did provide a timeout value on
DataFlowVariable#getVal():
ActionEvent e = event.getVal(4, TimeUnit.SECONDS)
I had you there for a second though, right? Be warned, if the timeout value is exceeded then getVal() returns null, it does not throw an exception.
The only downside to using DataFlow concurrency that I can see is that you actually need to understand a little bit about DataFlow concurrency. Who would have thought? DataFlowVariable is just the tip of the iceberg when it comes to GPars DataFlow support, and is not even the primary use case. The Dataflow User Guide is a good read and will explain a lot more about the concepts and GPars' implementation.
Feel free to try this yourself. You don't even need to download any jars or change your project files. Just add the correct Grapes to the top of your unit test:
@Grab(group='org.codehaus.gpars', module='gpars', version='0.9')
@GrabResolver(name='jboss', root='http://repository.jboss.org/maven2/')
If you need more help, then check out the full example.
Happy Testing!
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)





