You need to wait for an event in a TestCase.
Use the addAsync() FlexUnit method.
This recipe extends the Create a FlexUnit TestCase recipe.
Testing behavior in a TestCase often involves waiting for asynchronous events. If the TestCase methods only concern synchronous events like property change events fired immediately when the property is set, no special handling is required. It is only when asynchronous events are involved that extra care needs to be taken. A common example would be waiting for a URLLoad to finish or a UIComponent to finish creation. This recipe will discuss the syntax and gotchas of handling events in a sample TestCase using the URLLoad class and a fictitious Configuration object. Testing UIComponents in FlexUnit is a more complex topic that is addressed in a separate recipe.
Events need to be treated specially in TestCases since unless FlexUnit is informed that it should be waiting for an event, as soon as the test method ends, it will think the method passed and start running the next test method. This can lead to false positive tests, where FlexUnit displays a green bar but behind the scenes a test silently failed or worse displayed an error dialog.
To inform FlexUnit that it should wait for an event to fire before marking a test as passed or failed, the listener passed to addEventListener() must be replaced by a call to addAsync(). The first two arguments to addAsync() are required while the remaining two are optional. The required first argument is the listener that should be called when the event is fired. This is the method that would have been used as the listener before introducing addAsync(). The required second argument is the timeout in milliseconds for waiting for the event to fire. Should the event not fire by the timeout, FlexUnit will mark the test as failed and continue running the other test methods.
A typical usage of addAsync() can be seen in the example below:
package
{
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flexunit.framework.TestCase;
public class ConfigurationTest extends TestCase
{
public function testParse():void
{
var urlLoader:URLLoader = new URLLoader();
urlLoader.addEventListener(Event.COMPLETE, addAsync(verifyParse, 1000));
urlLoader.load(new URLRequest("sample.xml"));
}
private function verifyParse(event:Event):void
{
var configuration:Configuration = new Configuration();
assertFalse(configuration.complete);
configuration.parse(new XML(event.target.data));
assertTrue(configuration.complete);
}
}
}
The testing of the parse method on the Configuration object has been broken up into two methods. The first method constructs the objects and initiates the action that requires waiting for the event. The verify method then uses the result of the event to perform its processing and make assertions. In normal code the verifyParse() method would have been directly used as the listener argument to addEventListener(), but here it was wrapped with addAsync() and given a 1000 millisecond timeout. Note that the name of the listener function doesn't start with test, if it did FlexUnit would attempt to run that method as an additional test which is not desirable.
The type of the event is honored by FlexUnit and the target listener can cast its argument as such. If instead of a generic Event being dispatched in the above example, a FlexEvent or some other Event subclass was used, the listener could safely define its parameter to be that type. Should a mismatch of event types occur a Type Coercion failure would be reported at runtime.
At this point there are two important caveats about addAsync() that need to be mentioned. First never have more than one addAsync() waiting at a time. The FlexUnit framework doesn't correctly handle detecting and failing a test if more than one addAsync() is defined. It is possible to chain addAsync() calls, such that in the listener called by one addAsync() a new addAsync() can be created as shown below. Secondly don't register an addAsync() for an event that will be fired multiple times during a test. Since the addAsync() mechanism is used as a rendezvous point for the FlexUnit framework to know when a test has finished or failed, having the same addAsync() called multiple times can produce false positives and odd behavior.
When using addAsync() instead of having to use a closure or create an instance variable, such information can instead be passed along to the listener by using the optional third argument of addAsync(). The passthrough data can be anything, providing flexibility in what data can be passed. For example the test defined above could be written to create and verify the complete flag of the Configuration object prior to initiating the XML load. Such an approach follows the fail fast pattern of unit tests helping to keep the running time of the entire test suite as short as possible. The modified test method using passthrough data would be written like this:
public function testComplete():void
{
var configuration:Configuration = new Configuration();
assertFalse(configuration.complete);
var urlLoader:URLLoader = new URLLoader();
urlLoader.addEventListener(Event.COMPLETE, addAsync(verifyComplete, 1000, configuration));
urlLoader.load(new URLRequest("sample.xml"));
}
private function verifyComplete(event:Event, configuration:Configuration):void
{
configuration.parse(new XML(event.target.data));
assertTrue(configuration.complete);
}
Here the object that was created in the test method is passed along to verifyComplete() as the second argument. Generic objects or primitive types like ints can also be passed using this mechanism.
By default if the event is not dispatched by the timeout specified a failure will be generated by Fl;exUnit. If instead some custom handling should occur if the event doesn't fire, the optional fourth argument to addAsync() is used to specify the function that should be called. Defining a custom failure handler is helpful when testing that an event didn't fire or to perform cleanup specific to the objects involved in the test. The custom failure handler will always be passed the passthrough data even if it is null. To test that the Configuration object never fires a complete event, it could be accomplished as such:
public function testCompleteEvent():void
{
var configuration:Configuration = new Configuration();
assertFalse(configuration.complete);
configuration.addEventListener(Event.COMPLETE, addAsync(verifyEvent, 250, configuration, verifyNoEvent));
}
private function verifyEvent(event:Event, configuration:Configuration):void
{
fail("Unexpected Event.COMPLETE from Configuration instance");
}
private function verifyNoEvent(configuration:Configuration):void
{
assertFalse(configuration.complete);
}
It is still necessary to define a listener for the event, but as shown in this example should that event fire it represents an error condition. The custom failure handler verifies that the Configuration is still in the proper state given that the event didn't fire.
If multiple asynchronous events will fire in order to setup or test an object it is important that only one addAsync() be active at a time, as mentioned above. To get around this limitation in the handling of one event another addAsync() can be created. Extending the example from above if the parsing of a configuration requires loading additional files the complete status may not immediately change. A sample of how these two events could be chained is shown below:
public function testComplexComplete():void
{
var configuration:Configuration = new Configuration();
assertFalse(configuration.complete);
var urlLoader:URLLoader = new URLLoader();
urlLoader.addEventListener(Event.COMPLETE, addAsync(verifyComplexParse, 1000, configuration));
urlLoader.load(new URLRequest("complex.xml"));
}
private function verifyComplexParse(event:Event, configuration:Configuration):void
{
configuration.addEventListener(Event.COMPLETE, addAsync(verifyComplexComplete, 1000, configuration));
configuration.parse(new XML(event.target.data));
assertFalse(configuration.complete);
}
private function verifyComplexComplete(event:Event, configuration:Configuration):void
{
assertTrue(configuration.complete);
}
In the verifyComplexParse() function which is configured in the first addAsync(), it makes a second call to addAsync() to setup the next event in the chain to listen for. Chaining can be done as many levels deep as needed.