How to use FlexUnit with FlexBuilder 2

| 15 Comments | No TrackBacks

A reader of mine asks, "Could you post an example of how you would utilize test driven design with ActionScript? I, like most developers, begin projects by coding first and planning later. I want to change this very bad habit but I am having trouble starting with test first."

When coding ActionScript 3, I do all of my testing with FlexUnit. Don't be confused by the "Flex" in the name - FlexUnit is fully capable of testing standalone ActionScript 3 code even though the name implies "Flex-only."

In order to get started, I'm going to walk you through the three step process of unit testing:

  • Create a "test runner" to run all tests and display the results.
  • Create the "test suite(s)" that contains the "test cases" to run. Each test is simply a method with a series of assertions to verify that the values the code is producing is what you expect the code to produce.
  • Create the class(es) that are tested by the test cases

First things first, you'll want to download FlexUnit. Because I'll be demonstrating using the flexunit.swc file, you'll want to grab the .zip archive containing the docs, source, and swc. Download and extract the flexunit.zip file to your harddrive. I'm using C:\Development\Flex as the extract location, which creates a directory C:\Development\Flex\flexunit containing the files.

After FlexUnit is downloaded / extracted, you'll need to create a Flex project to compile the test runner. FlexUnit comes with beautiful test runner GUI, but because the test runner is written with MXML it needs to be run through a Flex project. If you're attempting to test an ActionScript 3-only project, you'll want to create a Flex project alongside of it to run the tests.

Open up FlexBuilder 2.0 Beta and create a new Flex project by selecting File -> New -> Flex Project.

  • Select "No" when asked about Flex Enterprise Services and press the "Next" button.
  • Give the project a name, I'm using "ExampleTestRunner" and creating the project in "C:\Development\Flex\ExampleTestRunner" with "ExampleTestRunner.mxml" as the main application file. Click the "Next" button.
  • Select the "Libraries" tab and press the "Add SWC" button.
  • Browse for the "flexunit.swc" file that was extracted from the original flexunit.zip. The location for me is "C:\Development\Flex\flexunit\bin\flexunit.swc". Press "OK".
  • Click the "Finish" button.

Adding the .swc to the build path of the project allows you to use the FlexUnit code. If you're not creating a project from scratch, you can add the flexunit.swc to the build path by right-clicking on the project name and selecting "Properties." Select "Flex Build Path", click the "Libraries" tab, and follow the rest of the steps outlined above.

Now that the project is set up correctly to reference the FlexUnit code, the first thing you'll want to do is create the code for the test runner. For this example, I'm going to focus on creating a TemperatureConverter class to convert from celsuis to fahrenheit and vice versa. The code for the test runner looks like this:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.macromedia.com/2005/mxml" xmlns="*"
				xmlns:flexunit="flexunit.flexui.*"
				creationComplete="onCreationComplete()">
	
	<mx:Script>
		<![CDATA[
			import flexunit.framework.TestSuite;
			
			// After everything is built, configure the test
			// runner to use the appropriate test suite and
			// kick off the unit tests
			private function onCreationComplete()
			{
				testRunner.test = createSuite();
				testRunner.startTest();
			}
			
			// Creates the test suite to run
			private function createSuite():TestSuite {
				var ts:TestSuite = new TestSuite();
				
				// TODO: Add more tests here to test more classes
				// by calling addTest as often as necessary
				ts.addTest( TemperatureConverterTest.suite() );
				
				return ts;
			}
			
		]]>
	</mx:Script>

	<!-- flexunit provides a very handy default test runner GUI -->
	<flexunit:TestRunnerBase id="testRunner" width="100%" height="100%" />
</mx:Application>>

There are some important things to note in the above code block. First, note the "flexunit" namespace declared in the <mx:Application> tag. This allows you to easily create the test runner. Also, once the application has been created, the "testRunner" has the test property set before starting the tests. Finally, the createSuite() method creates a test suite, and adds all of the tests to it.

You'll notice that the test suite we're using is going to be called "TemperatureConverterTest". Since the test runner is set up at this point, the next step is to create the test suite. Create a new ActionScript file named "TemperatureConverterTest.as" and use the following code:

package {

	import flexunit.framework.TestCase;
	import flexunit.framework.TestSuite;
	
	public class TemperatureConverterTest extends TestCase {
		
	    public function TemperatureConverterTest( methodName:String ) {
			super( methodName );
        }
	
		public static function suite():TestSuite {
			var ts:TestSuite = new TestSuite();
			
			ts.addTest( new TemperatureConverterTest( "testToFahrenheit" ) );
			ts.addTest( new TemperatureConverterTest( "testToCelsius" ) );
			return ts;
		}
		
		/**
		 * Ensures the celsius to fahrenheit conversion works as expected.
		 */
		public function testToFahrenheit():void {
			// Test boiling point
			var celsius:Number = 100;
			var fahrenheit:Number = TemperatureConverter.toFahrenheit( celsius );
			assertTrue( "Expecting 212 fahrenheit", fahrenheit == 212 );
			
			// Test freezing point
			var celsius:Number = 0;
			var fahrenheit:Number = TemperatureConverter.toFahrenheit( celsius );
			assertTrue( "Expecting 32 fahrenheit", fahrenheit == 32 );
		}
		
		/**
		 * Ensures the fahrenheit to celsius conversion works as expected.
		 */
		public function testToCelsius():void {
			// Test boiling point
			var fahrenheit:Number = 212;
			var celsius:Number = TemperatureConverter.toCelsius( fahrenheit );
			assertTrue( "Expecting 100 celsius", celsius == 100 );
			
			// Test freezing point
			fahrenheit = 32;
			celsius = TemperatureConverter.toCelsius( fahrenheit );
			assertTrue( "Expecting 0 celsius", celsius == 0 );
		}
		
	}
}

Notice the use of assertTrue() - this is the key to assuring the values are as expected. There is a conditional passed as the second parameter. You're asserting that the conditional should result in the value of true with the assertTrue() method. If the value doesn't result in true, the assertion is going to fail, and the test runner will let you know about it.

Along with assertTrue(), you can also use assertFalse(), assertNull(), etc. The full list is available in the assert documentation

Since the tests are now written, the next step is to create the TemperatureConverter class so the project will compile. The first thing to do is create stub methods that return generic values just so you can get the project to compile. The stubbed TemperatureConverter class looks like this (place this code in TemperatureConverter.as):

package {
	
	public class TemperatureConverter {
		
		public static function toFahrenheit( celsius:Number ):Number {
			return 0;
		}
		
		public static function toCelsius( fahrenheit:Number ):Number {
			return 0;
		}
	}
}

The project is ready to compile at this point. Click the "Run" button (green play button looking thing in the FlexBuilder toolbar). The test runner launches and immediately you can see there are two test failures:

Believe it or not, these failures are a good thing. You know there are problems with your code and the test runner tells you precisely what tests failed. Now that the project compiles, it's time to go back and finish writing the TemperatureConverter, replacing the stub methods with real calculations:

package {
	
	public class TemperatureConverter {
		
		public static function toFahrenheit( celsius:Number ):Number {
			return ( 9 / 5 ) * celsius + 32;
		}
		
		public static function toCelsius( fahrenheit:Number ):Number {
			return ( 5 / 9 ) * ( fahrenheit - 32 );
		}
	}
}

After modifying the TemperatureConverter class and running the tests again, you can see that the tests pass this time:

test_pass_small.png

You can be reasonably sure that your code does in fact do what you want it to now because the tests created for it all pass without error. The next step would be creating even more tests, trying to test anything and everything. Before adding new classes to the project, you'll want to create the tests for the classes first. It's ok for the tests to fail initially because it gives you targetted goals to work towards during development.

This entry demonstrated how to get started with test driven design using ActionScript 3 and Flex 2. Hopefully you'll take this information and start applying it towards your own development practices. Good luck!

TAGS: , , , , , ,

No TrackBacks

TrackBack URL: http://www.darronschall.com/mt/mt-tb.cgi/98

15 Comments

  • Darron

    Thanks a lot for this comprehensive heads-up in TDD.

    Best regards
    Daniel

     
  • Great writeup on the new library. Thanks, Darron. This is just another great reason for developers to consider/reconsider using Flex2 for enterprise-level apps.

    Would it be possible to see an example of using flexunit with one/some of the corelib utilities?

     
  • If you want to using FlexUnit with the corelibs, just look at the source code in the repository. All of the core libraries should have unit tests for them. For example, here's the tests for the StringUtils library:

    http://labs.macromedia.com/svn/flashplatform/?/projects/corelib/trunk/src/actionscript3/com/macromedia/utils/string/tests/StringUtilsTest.as

     
  • http://www.darronschall.com/weblog/images/test_fail_large.cfm

    Is it just me, or does the call stack look a bit excessive? The repeating pattern made me look at the source code.

    In flexunit.framework.TestSuite,

    runWithResult() calls
    runNext(), which calls
    runTest(), which calls
    runWithResult(), which calls...

    and so on. It seems that every test run increases the call stack by three, and then there's the circular recursion.

    There is this comment in the runNext() method:

    "note that the TestSuiteListener will make sure that runNext is called only occasionally so that recursion doesn't spiral out of control"

    And this comment in TestSuiteTestListener:

    "the Timer allows us to let frames progress and kill the deep recursion problem"

    Couldn't this be solved by using a humble array and a for loop, or am I on crazy pills? Why is an iterator being used here? What's the benefit? And surely one can use an iterator without circular recursion? I apologize if I am misunderstanding something.

     
  • Thanks for the insightful comment Robert. It's probably better to reproduce your comment on the i2 weblog, since they were the one who originally wrote the FlexUnit code. I assume that iterators are used because the code war originally ported from Java.

    The URL to direct comments to would be:
    http://www.richinternetapps.com/archives/000151.html

     
  • Thanks for the suggestion, Darron, but the blog's comments aren't enabled and I can't find an email address.

     
  • Hi Robert,

    I haven't looked at FlexUnit, but I've been using ASUnit (for AS2) a lot recently and I actually introduced an iterator, in a fairly brutal hack-up of the source code. I did this so that I could queue all the tests and run them sequentially, even if they were asynchronous. In the standard ASUnit code, asynchronous tests tend to run in parallel. I felt this was not ideal, since there are a number of factors which could cause them to interfere, and I wanted to ensure that, in all cases, the previous test has torn down before the next sets up.

     
  • Correction: It's just been pointed out to me that I missed the point of what you were saying, Robert. That is, the iterator is not itself a bad thing, but the particular implementation results in an escalating call-stack.

    Also, my discussion itself was not clear. The benefit of an iterator for me, was to allow the test runner to call runNext() in a loop while(currentTestSynchronous()), then to be able to jump back into the same position of the loop when an asynchronous test returns. An iterator is just a very clear way to express what is happening.

     
  • Hi,
    I am just trying your example with beta 2 and get the following error:

    ReferenceError: Error #1065: Variable http://www.adobe.com/2006/flex/mx/internal::_TestRunnerBase_StylesInit is not defined
    at flexunit.flexui::TestRunnerBase$iinit()
    ...

    Is this a problem because things changed in flex beta 2?

     
  • I am running the FlexUnit test with the Flex SDK and I never get any Stack Trace data when my test fail.

     
  • The flexunit.zip doesn't contain the SWC. Any one know how to get that file either by generating it or download?

     
  •  
  • If the stack trace is not showing as you expect, make sure of a couple things:
    - 1. A test is actually failing
    - 2. You have a debug version of the flash player installed. It can be downloaded from...

    http://www.adobe.com/support/flashplayer/downloads.html

    Thanks,
    Kris

    btw... thanks for posting a great example to get people running on Flex with test driven development.

     
  • Theo Hultberg asserts that TestSuites can be created automatically: http://blog.iconara.net/2007/02/06/flexunit/

    In TemperatureConverterTest, you don't need the public static function suite(), or even the constructor.

    In testRunner, you just use var ts:TestSuite = new TestSuite( TemperatureConverterTest );

    Hultberg's suggestion worked fine for me. Has anyone else tried it?

     
  • *ahem*

    When I first wrote this tutorial, FlexUnit didn't support that syntax for creating unit tests.

    The proper way to do it with "today's" codebase is as Theo suggests. You can see this in action in the test cases for as3corelib:

    http://as3corelib.googlecode.com/svn/trunk/tests/CoreLibTestRunner.as

     

Leave a comment

Flex.org - The Directory for Flex

Archives