Thursday, September 3, 2009

Using FlexUnit to Unit Test Cairngorm Visual Objects

Introduction
In a RIA application a typical split of the source code (counted by code size, in bytes) is as follows:
  • 45-60% - client: visual objects like views and their sub-components with their logic
  • 15-20% - client: non-visual objects (primarily Cairngorm worker-to-service piping)
  • 30-35% - server: middle-tier code
In the couple medium-size business application, I have analyzed, the middle-tier was implemented in Java and the client was implemented in Flex using the Cairngorm architecture. So, I don't have verified numbers for other RIA configurations.

If we are determined to use TDD or simple just provide automated tests for our application and get the most coverage for the effort required to develop these tests, it seems we could use the following testing methodologies:
  • FlexUnit testing for the client, especially the client's visual objects
  • FlexMonkey tests for whole client's features
  • jUnit tests for the middle-tier code.
I another blog, I will touch on the FlexMonkey. In this one, I'd like to present how we can unit test the visual layer of the client using the FlexUnit framework. jUnit testing is well described in many other sources.

Testing Scenario
In the spirit of Cairngorm, a UI user gesture is directed to a handler which creates a Cairngorm message, attaches a payload to it (any relevant information) and dispatches it (event.dispatch()). Our test will mimic a user gesture (click a button) and will try to catch the dispatched Cairngorm event and verify its payload. We're using Flex 3 (ver. 3.2) and FlexUnit 4 beta 2.

Sample Application
To explain the topic, I'll be using the following sample application. The view (Main.mxml adds it to the ViewStack) is (to see the code in a new window, move your mouse pointer over the code and click the left of the icons that appear in upper-right corner):
<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml"
width="400" height="300">

<mx:Script source="MyViewScript.as" />

<mx:HBox verticalAlign="middle" >
<mx:Text id="txtValue" text="Entry:" />
<mx:TextInput id="entryTextInput" />
<mx:Button id="btnSave" label="Save" click="onBtnSaveClick(event)" />
</mx:HBox>

</mx:VBox>

The script used bu the view is:
import gov.olcc.fishbait.events.SaveEntryEvent;
import gov.olcc.fishbait.model.ViewModelLocator;
import gov.olcc.fishbait.vo.EntryVO;

public var modelLocator:ViewModelLocator = ViewModelLocator.getInstance();

public function onBtnSaveClick(event):void
{
trace("clickHandler detected an event of type: " + event.type);

var entryVO: EntryVO = new EntryVO();
entryVO.text = entryTextInput.text;

var saveEvent: SaveEntryEvent = new SaveEntryEvent(entryVO);

saveEvent.dispatch();
}


and the TestRunner class to run the test is
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
xmlns:flexunit="http://www.adobe.com/2009/flexUnitUIRunner"
xmlns:business="gov.olcc.fishbait.business.*"
xmlns:control="gov.olcc.fishbait.control.*"
xmlns:view="gov.olcc.fishbait.view.*"
layout="vertical"
width="100%" height="100%"
creationComplete="runTests()">

<mx:Script>
<![CDATA[
import com.adobe.cairngorm.control.FrontController;
// import gov.olcc.fishbait.view.TestFishBaitView;
import gov.olcc.fishbait.view.MyView;
import org.flexunit.runner.FlexUnitCore;
import org.flexunit.flexui.TestRunnerBase;
import gov.olcc.fishbait.view.MainTest;
// import gov.olcc.fishbait.view.TestFishBait;

//Add an import statement(s) for the class(es) under test
private var core: FlexUnitCore;

private function runTests():void
{
core = new FlexUnitCore();
core.addListener(testRunner);
core.run(MainTest);
}

]]>
</mx:Script>

<!-- Cairngorm Controller and Service Locator -->
<control:FishBaitController id="controller" />
<business:Services id="services" />

<flexunit:TestRunnerBase id="testRunner" width="100%" height="100%" />
</mx:Application>

The trick is to be able to catch the Cairngorm event. What is not obvious, the event.dispatch() method uses a CairngormEventDispatcher, so we need to add our listener to it. The resulting test code is
package gov.olcc.fishbait.view {
import com.adobe.cairngorm.control.CairngormEventDispatcher;
import flash.events.MouseEvent;
import gov.olcc.fishbait.events.SaveEntryEvent;
import gov.olcc.fishbait.model.ViewModelLocator;
import gov.olcc.fishbait.vo.EntryVO;
import mx.automation.codec.AssetPropertyCodec;
import org.flexunit.Assert;
import org.fluint.uiImpersonation.UIImpersonator;
import org.flexunit.async.Async;

public class MainTest {
var myView: MyView;
public var modelLocator:ViewModelLocator = ViewModelLocator.getInstance();

[Before(async,ui)]
public function setUp():void {
myView = new MyView();
UIImpersonator.addChild(myView);
}

[After(async,ui)]
public function tearDown():void {
UIImpersonator.removeChild( myView );
}   

[Test(async,timeout="3000")]
public function testOnBtnSaveClick():void {
CairngormEventDispatcher.getInstance().
addEventListener(SaveEntryEvent.SAVE_TEXT,
Async.asyncHandler( this, handleTestOnBtnSaveClick, 3000 ),
false,0,true);

myView.entryTextInput.text = "Test Text";
var clickEvent: MouseEvent = new MouseEvent(MouseEvent.CLICK);
myView.btnSave.dispatchEvent(clickEvent);
}

private function handleTestOnBtnSaveClick(event:SaveEntryEvent,
passThroughData:Object):void {
var entry:EntryVO = event.text;
Assert.assertEquals("Test Text", entry.text);
}
}
}

Other points about this test:
  • We use FlexUnit UIImpersonator as a parent to our view. Without adding a view to a parent, the UI controlls are not initialized and can't be used.
  • To get the instance of the CairngormEventDispatcher, we use its static mathod getInstance().
  • Since the actual test happens in the event handler, we need to user an annotation [Test(async,timeout="xxx ms"] to request the Async support for the test.
  • Then, instead of providing just the handler method as an argument to the assEventListener(), we wrap this method in Async.asyncHandler(), which will wait for the handler to be called unless the handler will not be called within specified time, in which case the test will fail.
And this is it. Enjoy testing Cairngorm visual objects.

1 comment:

nek said...

Hey! Nice post. Got a question. I'm familiar with most of the testing metadata tags but what does [Before(async,ui)] mean?