Friday, February 19, 2010

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
} as ActionListener)
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)

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='')
If you need more help, then check out the full example.

Happy Testing!


pniederw said...

Is there any sensible output if the assertion fails in your first code example? Suppose the second approach wins in this respect too.

Hamlet D'Arcy said...

The output is sensible in the first code example (JConch): timeout failure. In the second you just get a NPE. I prefer the JConch approach, but it probably is not that important.

pniederw said...

I'm talking about the case where the following assertion fails:
assert e.actionCommand == "click"

I suppose the first approach would result in a timeout failure (without leaving a clue that the assertion was evaluated but failed), and the second would give a nice assertion output?

Hamlet D'Arcy said...

Well, putting the assertion in the event handler will give you a much nicer stack trace because it is a trace to the problem, not to where the assertion is. But throwing exceptions off thread in a unit test is kinda a bad idea. what if the exception gets eaten? then a failure does not happen even though you will see (if you are lucky) a stack trace.

pniederw said...

>Well, putting the assertion in the event handler will give you a much nicer stack trace

But most likely you won't ever get to see this stack trace...

>But throwing exceptions off thread in a unit test is kinda a bad idea. what if the exception gets eaten?

That's why I don't like the first approach. The meaningful (assertion) exception gets eaten, and I'm left with a timeout failure that leaves the impression that the assertion was never evaluated.

pniederw said...

Here is a suggestion to improve the first approach:
Support something like...
coord.finishTest {
assert "click" == e.actionCommand
...which will rethrow any exception caught by coord.finishTest() from coord.delayTestFinish().

Oracle Fusion Cloud HCM Online Training said...

We are the leading oracle fusion financials online training institute. we have a policy that is regarding student development which we care more about understanding of oracle concepts. we would launch a new course when technological changes occur in oracle.

Oracle Fusion HCM Training