August 27, 2008

Odd addAsync() behavior when handling asynchronous events

There is some subtle behavior in FlexUnit's addAsync() that can lead to false positives and false negatives. In each case code that one expects to be called fails to run, which means that your tests may not be covering everything that you hoped they would. To kick off this discussion I'll present some code that should fail, but doesn't.

package com.neophi.test {
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flexunit.framework.TestCase;
    import mx.core.Application;

    public class AsyncTest extends TestCase {
        private var count:int;
        private var eventDispatcher:EventDispatcher;

        public function testAsyncLater():void {
            eventDispatcher = new EventDispatcher();
            eventDispatcher.addEventListener(Event.COMPLETE, addAsync(stage1, 100));
            Application.application.callLater(eventDispatcher.dispatchEvent, [new Event(Event.COMPLETE)]);
        }

        private function stage1(event:Event):void {
            count++;
            if (count == 1) {
                // This code will not cause the listener function to be called again
                Application.application.callLater(eventDispatcher.dispatchEvent, [new Event(Event.COMPLETE)]);
            } else {
                fail("Fail");
            }
        }
    }
}

This example creates an addAsync() wrapped function to listen for Event.COMPLETE. The application's callLater() is used to simulate the asynchronous firing of that event at some point in the future. The first time stage1() is called the count increments and another event is fired. When this second event is handled the test should fail but doesn't. The reason is that another addAsync() wasn't registered with FlexUnit. When the second event fires FlexUnit quietly ignores it, leading to a false positive. To correct this situation we need to register a new addAsync() that listens for the second event. Luckily FlexUnit's addAsync() always returns the same function reference so there is no need to remove the first listener, calling addEventListener() again just updates the existing listener. The rewritten stage1() function looks like this:

private function stage1(event:Event):void {
    count++;
    if (count == 1) {
        // This code works correctly
        eventDispatcher.addEventListener(Event.COMPLETE, addAsync(stage1, 200));
        Application.application.callLater(eventDispatcher.dispatchEvent, [new Event(Event.COMPLETE)]);
    } else {
        fail("Fail");
    }
}

This test now correctly fails when run. The false negative happens if we forgot to use callLater() to fire the second event. An unexpected failure message results if stage1() was changed as follows:

private function stage1(event:Event):void {
    count++;
    if (count == 1) {
        eventDispatcher.addEventListener(Event.COMPLETE, addAsync(stage1, 200));
        // This code will cause an asynchronous function timeout error
        eventDispatcher.dispatchEvent(new Event(Event.COMPLETE));
    } else {
        fail("Fail");
    }
}

The test fails with the message: "Error: Asynchronous function did not fire after 200 ms". Internally FlexUnit makes a check to see if it should be waiting for an event, if it isn't expecting one, the new event is discarded. In this case I find the failure message misleading. It wasn't that the asynchronous function wasn't called, just that it wasn't called asynchronously. This scenario can easily crop up if you are using mock objects and forgot to delay an operation that normally fires events asynchronously, like simulating an HTTP GET request. Using the application's callLater() is a quick way to introduce the needed asynchronous behavior.

Tags: addasync asynchronous flexunit