Thursday, March 25, 2010

Event testing helper for FlexUnit 4

I noticed a little while ago that assertions made in event handlers don’t work the way you’d expect in FlexUnit.  It looks like the event dispatcher wraps its calls to event handlers with a try/catch block and treats any errors it receives as unhandled (showing the exception dialog if you have the debug player installed, ignoring it otherwise).  This means if you have an assertion fail in an event handler it will not cause the test to fail.

For example, here’s a simple test I had to verify that an event was raised and that the event data was correct:

[Test]
public function testUpdateAutoDialSuccessResponse():void
{
  var audioModel:AudioConferenceModel = new AudioConferenceModel();
 
  var phoneID:String = "567888";
  var errorCode:Number = 0;
  var errorMessage:String = null;
 
  audioModel.addEventListener(AudioConfAutoDialResponseEvent.SUCCESS,
      function(e:AudioConfAutoDialResponseEvent):void {
          Assert.assertEquals("PhoneID", phoneID, e.phoneID);
          Assert.assertEquals("Code", errorCode, e.errorCode);
          Assert.assertEquals("Message", errorMessage, e.errorMessage); 
      });
 
  audioModel.updateAutoDialInfo(phoneID, errorCode, errorMessage);
}

Even if the assertions in the event listener fail the test will show as passed, as the event dispatcher will swallow the event (showing an error dialog to the user if running in the debug player).

The other problem with using a normal event listener is that you have to do extra work if you want to verify that the event actually happened or not (or that certain events that you don’t want didn’t happen).  For instance, in this case since the error code is 0 we expect to get a SUCCESS event, so we'd like to verify that the FAILURE event isn't raised while also making sure SUCCESS is.  We could do something like this:
var seenFailure:Boolean = false;
var seenSuccess:Boolean = true;

audioModel.addEventListener(AudioConfAutoDialResponseEvent.FAILURE,
    function(e:AudioConfAutoDialResponseEvent):void {
      seenFailure = true;
    });
audioModel.addEventListener(AudioConfAutoDialResponseEvent.SUCCESS,
    function(e:AudioConfAutoDialResponseEvent):void {
      // asserts here
      seenSuccess = true; 
    });
 
audioModel.updateAutoDialInfo(phoneID, errorCode, errorMessage);

Assert.assertTrue("Seen Success", seenSuccess);
Assert.assertFalse("Seen Failure", seenFailure);

but having to do this every time is kind of annoying.

We could change this code to use FlexUnit’s asynchronous support, but that’s a bit problematic.  First of all, FlexUnit seems to expect the events to occur in the order that you register them with the async support, even for events that you claim should fail.  Say we have this:
[Test]
public function testUpdateAutoDialSuccessResponse():void
{
  var audioModel:AudioConferenceModel = new AudioConferenceModel();
 
  var phoneID:String = "567888";
  var errorCode:Number = 0;
  var errorMessage:String = null;
 
  Async.failOnEvent(this, audioModel, AudioConfAutoDialResponseEvent.FAILURE);
  Async.handleEvent(this, audioModel, AudioConfAutoDialResponseEvent.SUCCESS,
      function(e:AudioConfAutoDialResponseEvent):void {
        Assert.assertEquals("PhoneID", phoneID, e.phoneID);
        Assert.assertEquals("Code", errorCode, e.errorCode);
        Assert.assertEquals("Message", errorMessage, e.errorMessage); 
      });
 
  audioModel.updateAutoDialInfo(phoneID, errorCode, errorMessage);
}

Even though we say that FAILURE is a failure condition, FlexUnit still fails when it receives the SUCCESS event because it expects FAILURE to happen first.  So while this allows us to put the asserts in the handler, you have to be careful about the order you add the handlers.  It also means you're requiring a particular order for the events, and your tests may not want to enforce that.

I’ve created a testsupport.EventChecker class to help get around these problems.  You pass the event dispatcher to the checker at construction time, and then use the expect() and fail() methods to tell it what events to expect and which to fail on.  Expected events can also have a handler associated with them, which will be called whenever that event is received.  Then at the end of the tests you call the assert() method, and it will assert that all expected events were seen and no fail events were seen.

To allow for asserts to be placed in the event handler the EventChecker wraps the handler in a try/catch.  Any errors that it receives (including assertion failures) are stored in an errors array.  When assert() is called it checks that array and if it has any errors stored it will raise the first one.  The array itself is made publicly available in case you want to have your code look at all the errors seen.

Here’s that same test from above modified to use EventChecker.  On top of what the original test was trying to verify it also verifies that the FAILURE event is not raised:

[Test]
public function testUpdateAutoDialSuccessResponse():void
{
  var audioModel:AudioConferenceModel = new AudioConferenceModel();
  
  var phoneID:String = "567888";
  var errorCode:Number = 0;
  var errorMessage:String = null;
  
  var ec:EventChecker = new EventChecker(audioModel);
  ec.fail(AudioConfAutoDialResponseEvent.FAILURE);
  ec.expect(AudioConfAutoDialResponseEvent.SUCCESS,
      function(e:AudioConfAutoDialResponseEvent):void {
          Assert.assertEquals("PhoneID", phoneID, e.phoneID);
          Assert.assertEquals("Code", errorCode, e.errorCode);
          Assert.assertEquals("Message", errorMessage, e.errorMessage);  
      });
 
  audioModel.updateAutoDialInfo(phoneID, errorCode, errorMessage);
    
  ec.assert();
}

The source for the EventChecker class can be found on github, embedded below.



Helper class for testing events in FlexUnit 4.

2 comments:

  1. Ok I'm a year late so this probably didn't exist when you wrote this, but you can also do this:

    Async.registerFailureEvent(this, audioModel, AudioConfAutoDialResponseEvent.FAILURE);

    Async.proceedOnEvent(this, audioModel, AudioConfAutoDialResponseEvent.SUCCESS, 10000);

    ReplyDelete