EladElrom.com

Deep Dive Into Technology

Test Driven Development using Flash Builder 4 beta and FlexUnit on Devnet

There has been a great deal of interest in TDD (and unit testing in general) recently, because it leads to applications that are easier to scale and maintain and less prone to errors. I just published an article on Devnet that covered unit testing and Test Driven Development using Flash Builder 4 and FlexUnit, including some of the new features in the FlexUnit 4 framework.

Check it out here:
http://www.adobe.com/devnet/flex/articles/flashbuilder4_tdd_02.html

FlexUnit Asynchronous tests with PureMVC Framework

There are very little resources online in term of how to use FlexUnit with PureMVC and TDD, furthermore in many cases it makes more sense to create the tests after user stories are implemented.

Let’s take the application we created for Cairngorm and convert it to PureMVC framework. This is a good exercise since it can help define the different between the two frameworks.

Creating tests with existing framework may be challenging at times. In fact, there are cases where it’s makes more sense to create the test after the code is completed rather than following common TDD practices.

TDD recommend to create the test before writing the code, however working in real life application and with exsiting frameworks you may find that sometimes it’s easier to write the code before the test, especially when you use plug-ins or code generator scripts to create your user gesture automatically. I recommend to use your own judgment and see what’s works best for you.

Take a look at the post I published yesterday:
http://eladelrom.com/blog/2008/12/29/tdd-and-asynchronous-tests-with-cairngorm-applications/

View application and download the source code:
CairngormPM and TDD

In this example I am using the same FeedsPanelViewer.mxml container I created for the Cairngorm application. However, I made few small change to the container. I decided to remove the binding properties and let the mediator handle all the changes in the container.

<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
     layout="vertical"
     width="600" height="450"
     styleName="panelView">

     <!-- Feeds List -->
     <mx:List
          id="feedList"
         change="dispatchEvent(new UserSelectedFeedEvent(FeedVO( feedList.selectedItem )));"
          labelField="title"/>

     <!-- Detail information -->
     <mx:VBox width="540" horizontalScrollPolicy="off" verticalScrollPolicy="off">
          <mx:Spacer height="15" />
          <mx:HBox>
               <mx:Label text="FEED DETAIL:" fontWeight="bold"/>
          </mx:HBox>
          <mx:HBox>
               <mx:Label text="author:" fontWeight="bold"/>
               <mx:Label id="author" width="100%"/>
          </mx:HBox>
     </mx:VBox>

</mx:Panel>

PureMVC Mediator:

package com.elad.TDDPureMVC.view
{
     import com.elad.TDDPureMVC.events.UserSelectedFeedEvent;
     import com.elad.TDDPureMVC.model.FeedsPanelViewerProxy;
     import com.elad.TDDPureMVC.model.vo.FeedVO;
     import com.elad.TDDPureMVC.view.components.FeedsPanelViewer;

     import org.puremvc.as3.interfaces.IMediator;
     import org.puremvc.as3.interfaces.INotification;
     import org.puremvc.as3.patterns.mediator.Mediator;

     public class FeedsPanelViewerMediator extends Mediator implements IMediator
     {
          public static const NAME:String = 'FeedsPanelViewerMediator';

          private var feedsPanelViewerProxy:FeedsPanelViewerProxy;

          public function FeedsPanelViewerMediator(viewComponent:Object=null)
          {
               super(NAME, viewComponent);
               feedsPanelViewer.addEventListener(UserSelectedFeedEvent.USERSELECTEDFEED_EVENT, changeSelectedFeed);
          }

          private function changeSelectedFeed(event:UserSelectedFeedEvent):void
          {
               setDetail(event.selectedFeed);
          }

          public function get feedsPanelViewer():FeedsPanelViewer
          {
               return viewComponent as FeedsPanelViewer;
          }

          override public function listNotificationInterests():Array
          {
               return [
                    FeedsPanelViewerProxy.READ_ADOBE_FEEDS_SUCCESS,
                       ];
          }

          private function setDetail(feed:FeedVO):void
          {
               feedsPanelViewer.author.text = feed.author;
               feedsPanelViewer.category.text = feed.category;
               feedsPanelViewer.description.text = feed.description;
               feedsPanelViewer.link.text = feed.link;
               feedsPanelViewer.pubdate.text = feed.pubdate;
          }

          override public function handleNotification(notification:INotification):void
          {
               feedsPanelViewerProxy = facade.retrieveProxy(FeedsPanelViewerProxy.NAME) as FeedsPanelViewerProxy;

               switch ( notification.getName() )
               {
                    case FeedsPanelViewerProxy.READ_ADOBE_FEEDS_SUCCESS:
                         feedsPanelViewer.feedList.dataProvider = feedsPanelViewerProxy.feedsCollectionVO.collection;
                         setDetail(feedsPanelViewerProxy.selectedFeed);
                         feedsPanelViewer.title = feedsPanelViewerProxy.panelTitle;

                    break;
               }
          }
     }
}

Proxy:

package com.elad.TDDPureMVC.model
{
     import com.elad.TDDPureMVC.model.vo.FeedVO;
     import com.elad.TDDPureMVC.model.vo.FeedsCollectionVO;

     import mx.rpc.events.FaultEvent;
     import mx.rpc.events.ResultEvent;
     import mx.rpc.http.HTTPService;

     import org.puremvc.as3.interfaces.IProxy;
     import org.puremvc.as3.patterns.proxy.Proxy;

     public class FeedsPanelViewerProxy extends Proxy implements IProxy
     {

          public static const NAME:String = "FeedsPanelViewerProxy";
          public static const READ_ADOBE_FEEDS_SUCCESS:String = 'readAdobeFeedsSuccess';
          public static const READ_ADOBE_FEEDS_FAILED:String = 'readAdobeFeedsFailed';
          public var service:HTTPService;

          public function FeedsPanelViewerProxy()
          {
               super(NAME, new FeedsCollectionVO() );

               service = new HTTPService();
            service.url = "http://rss.adobe.com/en/resources_flex.rss";
            service.resultFormat = "e4x";
               service.addEventListener( FaultEvent.FAULT, onFault );
               service.addEventListener( ResultEvent.RESULT, onResult );
          }

          public function getAdobeFeeds():void
          {
               service.send();
          }

          // Cast data object with implicit getter
          public function get feedsCollectionVO():FeedsCollectionVO
          {
               return data.feedsCollectionVO as FeedsCollectionVO;
          }

          public function get selectedFeed():FeedVO
          {
               return (data.feedsCollectionVO as FeedsCollectionVO).collection.getItemAt(0) as FeedVO;
          }

          public function get panelTitle():String
          {
               return data.panelTitle as String;
          }

          private function onResult( result:ResultEvent ) : void
          {
               var feed:FeedVO;
               var item:Object;
               var len:int = result.result[0].channel.item.length();
               var collection:FeedsCollectionVO = new FeedsCollectionVO;
               var dataObject:Object = new Object();

               for (var i:int=0; i<len; i++)
               {
                    feed = new FeedVO();
                    item = result.result[0].channel.item[i];

                    feed.author = item.author;
                    feed.category = item.category;
                    feed.description = item.description;
                    feed.link = item.link;
                    feed.pubdate = item.pubdate;
                    feed.title = item.title;

                    collection.addItem(feed);
               }

               dataObject.feedsCollectionVO = collection;
               dataObject.panelTitle = String(result.result.*[0].*[0]);

               setData(dataObject);
               sendNotification(READ_ADOBE_FEEDS_SUCCESS);
          }

          private function onFault( event:FaultEvent) : void
          {
               sendNotification( READ_ADOBE_FEEDS_FAILED, event.fault.faultString );
          }

     }
}

To test the PureMVC framework I will be using puremvc-flexunit-testing. Our test calls the registerObserver method used for listening for a PureMVC notification on a proxy. We pass the PureMVC view, proxy and information for the AddSync such as the method to send a success response and timeout.

package flexUnitTests.proxies
{
     import com.andculture.puremvcflexunittesting.PureMVCNotificationEvent;
     import com.andculture.puremvcflexunittesting.PureMVCTestCase;
     import com.elad.TDDPureMVC.model.FeedsPanelViewerProxy;

     import flexunit.framework.Assert;

     import org.puremvc.as3.core.View;
     import org.puremvc.as3.interfaces.IView;

     public class ReadAdobeFeedsTestCase extends PureMVCTestCase
     {
          override public function setUp():void
          {
               var facade:ApplicationFacade = ApplicationFacade.getInstance();
               facade.registerProxy( new FeedsPanelViewerProxy() );
          }

          private function get proxy():FeedsPanelViewerProxy
          {
               var retVal:FeedsPanelViewerProxy = ApplicationFacade.getInstance().retrieveProxy(FeedsPanelViewerProxy.NAME) as FeedsPanelViewerProxy;
               return retVal;
          }

          private function get view():IView
          {
               return View.getInstance();
          }

          public function ReadAdobeFeedsTestCase(methodName:String=null)
          {
               super(methodName);
          }

          public function testReadAdobeFeedsEvent():void
          {
               registerObserver(this.view, this.proxy, FeedsPanelViewerProxy.READ_ADOBE_FEEDS_SUCCESS, handleResponse, 300);
               this.proxy.getAdobeFeeds();
          }

          private function handleResponse(e:PureMVCNotificationEvent):void
          {
               Assert.assertEquals("Feed title is incorrect", proxy.panelTitle, "Flex News");
          }
     }
}

TDD and Asynchronous Tests with Cairngorm applications

Model-View-Controller (MVC) frameworks such as PureMVC or Cairngorm and TDD makes a good marriage. MVC framework works very well with TDD since the application logic is separated from the front view and the data. TDD allows us to create Test Cases on the logic only, as well as create a test case for the presentation layer.

With that said, Cairngorm by default doesn’t separate the view, behavior and model in each container. In order to utilize TDD it’s recommended to use the Presentation model pattern. Few blogs back I covered Cairngorm and the presentation model so in this blog post I am going to continue by showing you how to use TDD and FlexUnit to test your user stories.

The application I am using is simple application with only three business rules:

1. Connect to adobe feeds and retrieve the latest feeds related to Flex.
2. Display these results in a list.
3. Upon user clicking an item on the list display detail information.

Take a look and download the source code:
CairngormPM and TDD

So far you can easily figure out how to create the User Story for item #1, To test Asynchronous tests in FlexUnit we can use the “addAsync” method. Here’s an example of calling the Flex News feeds.

package flexUnitTests
{
    import flexunit.framework.TestCase;

    import mx.rpc.events.ResultEvent;
    import mx.rpc.http.HTTPService;

    public class ServiceTestCase extends TestCase
    {
        private var service:HTTPService;

        public function ServiceTestCase(methodName:String=null)
        {
            super(methodName);
        }

        override public function setUp():void
        {
            service = new HTTPService();
            service.url = "http://rss.adobe.com/en/resources_flex.rss";
            service.resultFormat = "e4x";
        }

        override public function tearDown():void
        {
            service = null;
        }

        public function testServiceCall():void
        {
            service.addEventListener(ResultEvent.RESULT, addAsync(serviceResultEventHandler, 2000, {expectedResults: "Flex News"}, failFunction));
            service.send();
        }

        private function serviceResultEventHandler(event:ResultEvent, data:Object):void
        {
            var val:String = event.result[0].channel.title;
            assertTrue("Unable to retrieve Flex News feeds", val, data.expectedResults);
        }

        private function failFunction(data:Object):void
        {
            fail("Unable to connect to Flex News feeds");
        }
    }
}

Essentially, we should create a test case for each Cairngorm user gesture. So the process is as follow:

1. Understand what the purpose of the user gesture and what model get effected
2. Dispatch the event and watch changes in the model.

Additionally, it’s recommended to create FlexMonkey view tests to ensure the application view is changing according to our requirement and will test the container.

I am not going to explain the entire code, let me cover one test case and you can figure out the rest;

The ReadAdobeFeedsTestCase this test case is based on testing ReadAdobeFeedsEvent. Each Cairngorm Event-Command is based on a user gesture and you need to understand what that user gesture means.

In our case we dispatch the event and on result we place the collection in the model, so we can dispatch the event and make sure the results reaches the model. Here’s the watcher:

watcherInstance = ChangeWatcher.watch(modelLocator.feedsPanelViewerPM,["feedsCollection"],
addAsync(itemsChanged, 2000, {compareResults: 0}, failFunc));
new ReadAdobeFeedsEvent().dispatch();

And once a change is made to the model the binding method will dispatch an event automatically and we can listen to that change and compare the result in the model:

private function itemsChanged(event:Event, data:Object):void
        {
            var len:int = modelLocator.feedsPanelViewerPM.feedsCollection.collection.length;
            Assert.assertTrue("Collection is empty", len>data.compareResults);
        }

Take a look at the complete code:

package flexUnitTests.events
{
    import com.elad.application.events.ReadAdobeFeedsEvent;
    import com.elad.application.model.ModelLocator;

    import flash.events.Event;

    import flexunit.framework.Assert;
    import flexunit.framework.TestCase;

    import mx.binding.utils.ChangeWatcher;

    public class ReadAdobeFeedsTestCase extends TestCase
    {

        [Bindable]
        private var modelLocator:ModelLocator = ModelLocator.getInstance();

        private var watcherInstance:ChangeWatcher;

        public function ReadAdobeFeedsTestCase(methodName:String=null)
        {
            super(methodName);
        }

        public function testReadAdobeFeedsEvent():void
        {
            watcherInstance = ChangeWatcher.watch(modelLocator.feedsPanelViewerPM,["feedsCollection"],
                addAsync(itemsChanged, 2000, {compareResults: 0}, failFunc));

            new ReadAdobeFeedsEvent().dispatch();
        }

        private function itemsChanged(event:Event, data:Object):void
        {
            var len:int = modelLocator.feedsPanelViewerPM.feedsCollection.collection.length;
            Assert.assertTrue("Collection is empty", len>data.compareResults);
        }

        private function failFunc(data:Object):void
        {
            fail("Couldn't connect to Adobe feeds and update application model");
        }
    }
}

We have to make some changes in the TestRunner.mxml container.
The reason is that we need to add the same references as we added in our main application to the singleton classes: Front Controller and Service Locator. Otherwise the application will not be able to map between the event and commands as well as lack the ability to make service calls.

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
	xmlns:flexunit="flexunit.flexui.*"
	xmlns:business="com.elad.application.business.*"
	xmlns:control="com.elad.application.control.*"
	creationComplete="onCreationComplete()">

	<mx:Script>
		<![CDATA[
			import flexUnitTests.GesturesTestSuite;

			import flexUnitTests.events.ReadAdobeFeedsTestCase;
			import flexUnitTests.events.UserSelectedFeedTestCase;
			import flexunit.framework.TestSuite;

			private function onCreationComplete():void
			{
				testRunner.test = currentRunTestSuite();
				testRunner.startTest();
			}

			public function currentRunTestSuite():TestSuite
			{
				var testsToRun:TestSuite = new TestSuite();

				testsToRun.addTest(GesturesTestSuite.suite());
				testsToRun.addTest(new UserSelectedFeedTestCase("testUserSelectedFeedEvent"));
				testsToRun.addTest(new ReadAdobeFeedsTestCase("testReadAdobeFeedsEvent"));
				return testsToRun;
			}

		]]>
	</mx:Script>

	<control:testController />
	<business:Services />
	<flexunit:TestRunnerBase id="testRunner"/>

</mx:Application>

download the source code

I will published a post soon on creating Test Suites and Test Cases for PureMVC, which is much easier than Cairngorm applications since the presentation containers are already split from the data and logic.