August 27, 2008

New Event Testing Framework in FlexUnit

Part of the new FlexUnit release is a new TestCase subclass called EventfulTestCase. It provides convenience methods for testing objects that emit events. This new class can streamline your testing code and help catch unexpected events. Below is some sample code that demonstrates this new framework.

To use the new event testing framework subclass EventfulTestCase instead of TestCase.

package com.neophi.test {
    import flexunit.framework.EventfulTestCase;

    public class EventTest extends EventfulTestCase {
        // Test code here
    }
}

In a particular test method once you have the object that you want to test for assertions against, you call a new helper function called listenForEvent(). It takes three arguments:

  • source: the object that is to be listened on for the dispatched event
  • type: the type of event that the source object might dispatch
  • expected: whether the event is expected to be dispatched or now; defaults to EVENT_EXPECTED, the other choice is EVENT_UNEXPECTED (both defined in EventfulTestCase)

After your code has exercised the source to emit events, you make a call to assertEvents() with an optional message to make sure the events happened as planned. A quick example of a positive test for an event would look like this:

public function testEventExpectedPass():void {
    var eventEmittingObject:EventDispatcher = new EventDispatcher();

    listenForEvent(eventEmittingObject, Event.COMPLETE, EVENT_EXPECTED);
    eventEmittingObject.dispatchEvent(new Event(Event.COMPLETE));

    assertEvents("Expecting complete event");
}

After the event source is setup, we notify the testing framework what event we are expecting, emit the event, and then assert that is happened. In a more typical example the dispatchEvent() call would be part of the code under test. Also you aren't limited to listening for just a single event at a time. This example is just demonstrating the test framework API. When setting up an expected event via listenForEvent() the third argument defaults to EVENT_EXPECTED.

If an expected event doesn't get dispatched, the test harness will report an error as this next test demonstrates.

public function testEventExpectedFail():void {
    var eventEmittingObject:EventDispatcher = new EventDispatcher();

    listenForEvent(eventEmittingObject, Event.COMPLETE, EVENT_EXPECTED);

    assertEvents("Expecting complete event");
}

Testing for unexpected events works the same way but the fail and pass conditions are reversed.

public function testEventUnexpectedPass():void {
    var eventEmittingObject:EventDispatcher = new EventDispatcher();

    listenForEvent(eventEmittingObject, Event.COMPLETE, EVENT_UNEXPECTED);

    assertEvents("Not expecting complete event");
}

In this case the COMPLETE event is unexpected so by not firing the event the assertion passes, which is what we want. Just to flesh out the example, if an event is unexpected but does fire the assertEvents() call will fail as in this example:

public function testEventUnexpectedFail():void {
    var eventEmittingObject:EventDispatcher = new EventDispatcher();

    listenForEvent(eventEmittingObject, Event.COMPLETE, EVENT_UNEXPECTED);
    eventEmittingObject.dispatchEvent(new Event(Event.COMPLETE));

    assertEvents("Not expecting complete event");
}

In this case because an unexpected event fired it is an error.

Besides listenForEvent() and assertEvents(), two properties are exposed for examining the events that are listened for: lastDispatchedExpectedEvent holds the last event captured by listenForEvent() and dispatchedExpectedEvents holds all of the events captured. This last example shows the ability to listen for multiple events in different states, how events not registered with the listener are ignore, and uses these two convenience properties.

public function testMultipleEventsPass():void {
    var eventEmittingObject:EventDispatcher = new EventDispatcher();

    listenForEvent(eventEmittingObject, Event.INIT, EVENT_EXPECTED);
    listenForEvent(eventEmittingObject, Event.COMPLETE, EVENT_EXPECTED);
    listenForEvent(eventEmittingObject, ErrorEvent.ERROR, EVENT_UNEXPECTED);

    eventEmittingObject.dispatchEvent(new Event(Event.INIT));
    eventEmittingObject.dispatchEvent(new Event(Event.CHANGE));
    eventEmittingObject.dispatchEvent(new Event(Event.COMPLETE));

    assertEvents("Multiple events");

    assertEquals(Event.COMPLETE, lastDispatchedExpectedEvent.type);
    assertEquals(2, dispatchedExpectedEvents.length);
}

Some closing notes:


  • The order that the events are added by listenForEvent() doesn't matter. The asserts just check that events were or were not fired.

  • The event assertion framework will work with asynchronous events. You'll just want to wait to call assertEvents() in the final handler for your test.

  • The number of times that an event is dispatched doesn't matter.

  • assertEvents() can be called multiple times.

Tags: as3 events flexunit

June 30, 2006

Events in Flex 2

Today I got a familiar error while working with some event code:

TypeError: Error #1034: Type Coercion failed: cannot convert flash.events::Event@35f04c1 to com.neophi.events.CustomEvent.

The short answer is that this error results from failing to implement a clone() method on a custom event class. The long answer is that most of the time not implementing a clone() method won't hurt you, based on how you use your custom events, but you really should create one. In fact the Actionscript 3 (AS3) documentation is pretty clear about this:

When creating your own custom Event class, you must override the inherited Event.clone() method in order for it to duplicate the properties of your custom class.

This is a time that I wish AS3 had abstract methods.

Why I say you don't really need a clone() method is that it only becomes an issue if you relay an event. The follow code is an example of event relaying:

private function relay(customEvent:CustomEvent):void
{
    dispatchEvent(customEvent);
}

In this case an event listener redispatches the event that it got. When this happens the event framework behind the scenes calls the clone() method to create a new instance of the event. If you don't have a clone() method and the next listener in the chain is expecting an instance of CustomEvent, the error from above happens. If instead that same listener was only expecting an instance of the Event class, it would work, but any additional information contained in CustomEvent class would be lost, including the ability to cast it to an instance of CustomEvent.

In most cases the clone() method is simple to implement; just transfer any constructor arguments to a new instance of the class. Something like:

override public function clone():Event
{
    return new CustomEvent(_message);
}

Based on the type of event you will probably have more arguments including the all important type and often the other standard event variables for bubbling and cancelability.

While I'm on the subject of events I'll also talk about Event metadata or annotations that you can apply to classes. Often if you have an AS3 class that can emit custom events you will add metadata like this at the top:

package com.neophi {
    import com.neophi.events.CustomEvent;

    import flash.events.EventDispatcher;

    [Event(name="customChange", type="com.neophi.events.CustomEvent")]

    public class EventSource extends EventDispatcher {
        // class code removed 
    }
}

The thing to keep in mind is that depending on how this class will be used you probably don't need that metadata. If this AS3 class isn't used in MXML you can skip the metadata. The metadata is only used to inform the compiler how to translate an MXML event attribute into the appropriate code.

For example with the above class and metadata you could use it in MXML like this:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" xmlns:neophi="com.neophi.*">
<mx:Script>
<![CDATA[
import com.neophi.events.CustomEvent;

private function customChanged(customEvent:CustomEvent):void
{
    output.text += "EventTest: " + customEvent + "\n";
}
]]>
</mx:Script>
<neophi:EventSource id="eventSource" customChange="customChanged(event);" />
<mx:Button label="Go" click="eventSource.source();"/>
<mx:TextArea id="output" width="100%" height="400"/>
</mx:Application>

Because cutomChange is defined in the metadata for the EventSource class, in MXML you can now assign a handler to that event. In this case the method customChanged will be called whenever our instance of EventSource dispatches that event.

The method the button is calling to cause eventSource to fire the event is:

public function source():void
{
    var customEvent:CustomEvent = new CustomEvent("Event Message");
    dispatchEvent(customEvent);
}

This code is part of the EventSource class listed above. A new instance of CustomEvent is created and then dispatched. At this point all listeners will get called.

If you tried instead to assign the customChange handler in MXML without the metadata in EventSource you would get a compile error like this:

Cannot resolve attribute 'customChange' for component type com.neophi.events.EventSource.

For events then the two rules of thumb are:

  1. always create a clone() method in any custom event class
  2. add event metadata if you will need to setup handlers for that event in MXML

Tags: actionscript3 as3 events flex programming