Unit Testing in AS3

Unit Testing in AS3

Unit testing is one of the core techniques of modern software development, encouraging modular design and coding and ensuring the quality of individual components within a greater whole. When each component within an architecture is properly tested, through automated tests that run against that component's interface, fitting them together is likely to work much better, because the other parts of the application will be using interfaces which are well tested.

Other language communities have, over the last decade or so, developed high quality unit testing frameworks. The most well known is probably the JUnit (Java) and NUnit (NUnit) collection of libraries, along with their counterparts in other languages. The xUnit approach is to decorate unit tests with attributes (.Net) or annotations (Java), and have a test runner find the tests and execute them all, collating the results. This means that you don't have to create a list of tests, which can easily get out of date; just edit, add or remove methods in the test classes and they will be run.

This article presents a unit testing framework which follows these principles. It is not an 'ASUnit', but it comes from similar ideas.

Note: if you want to treat this article as a tutorial and code along, skip to the bottom and download the library first.

I. Defining Tests

The principles of unit testing and the mechanism through which my library works are best illustrated with an example. For this example I will use complex numbers, which are a simple and well defined component. Let's write an AS3 module (that is, a class) which implements the arithmetic operations for complex numbers.

Here are the operations we will want:

public class Complex {
	private var _r:Number, _i:Number;
	public function Complex(real:Number, imaginary:Number) : Complex {
		_r = real; _i = imaginary;
	}
		
	public static function add(c1:Complex, c2:Complex) : Complex { ... }
	public static function subtract(c1:Complex, c2:Complex) : Complex { ... }
	public static function multiply(c1:Complex, c2:Complex) : Complex { ... }
	public static function divide(c1:Complex, c2:Complex) : Complex { ... }
	public function get conjugate() : Complex { ... }
	
	public function toString() : String { return '[Complex, r=' + _r + ' i=' + _i + ']'; }
}

At this stage it doesn't matter what the implementation for those methods is. If you want to make it compile, you can have every function return new Complex(0, 0). (The string representation is important as that's what test comparisons revert to on user-defined types.) Perhaps we don't yet know what the correct definition of these operations is. However, by building up tests for each function we can develop requirements for the implementation, through the principles of Test Driven Development (TDD). Let's define some tests for the Add function, which is simple:

import RedCorona.Tests.TestCase;
public class ComplexTests {
	[Test]
	public function add_zero_leaves_value_same() : void{
		var c:Complex = Complex.add(new Complex(5, -2), new Complex(0, 0));
		TestCase.current.assertEqual(new Complex(5, -2), c);
	}
	
	[Test]
	public function add_two_complex() : void {
		var c:Complex = Complex.add(new Complex(5, -2), new Complex(3, 3));
		TestCase.current.assertEqual(new Complex(8, 1), c);	
	}
}

TestCase is a class which provides some useful equality assertions, which can be used on base types, arrays, dynamic objects and also, as we are doing here, user-defined class instances which define a string representation. If the two values are not equal, the test will fail – more about what that means below.

You may not recognise that [Test] notation, but ActionScript 3 allows you to place arbitrary metadata on classes and methods in this way. There are some special metadata values, such as [Embed(...)] or [Frame(...)], but [Test] is not one of them.

This definition of the tests is rather clumsy, as we have duplicated the entire test method apart from the arguments. I also provide the ability to define the arguments to the test function in the metadata:

import RedCorona.Tests.TestCase;
public class ComplexTests {
	[Test(5, -2, 0, 0, 5, -2)]
	[Test(5, -2, 3, 3, 8, 1)]
	public function add(r1:Number, i1:Number, r2:Number, i2:Number, r_ans:Number, i_ans:Number) : void {
		var c:Complex = Complex.add(new Complex(r1, i1), new Complex(r2, i2));
		TestCase.current.assertEqual(new Complex(r_ans, i_ans), c);
	}
}

This allows you to create several test cases for the same functionality, to make sure you aren't seeing a test pass because of the configuration you picked. The arguments to be passed will be reported as part of the test name, so you can see which test case failed.

So far we have defined a class, and some tests that we want to run against that object. So how do we include this test class in our application so that it runs?

II. Running Tests

The simplest way to run tests within a Flash application is to use the UI provided in the TestInterface class. (It requires some UI classes for the controls it uses; this is a pure AS3 solution so there is no dependency on Flex.) This can be added to the stage, and provides a Run button which can be used to run tests.

I typically place the unit tests inside a CONFIG::debug block so they are linked in the debug version of my SWF, but not the production version. In your main class entry point, you can simply do:

CONFIG::debug {
	var ti:TestInterface = new TestInterface(640, 480, new <Class> [ 
		ComplexTests
	] );
	addChild(ti);
}

This will give you something similar to the following, after pressing the Run button:

Unit test UI after running tests

The unit test UI after running some tests. The top panel shows output from the test runner about each test class and test method which it has run, and the bottom panel shows only the tests which failed. When all tests pass, the bottom panel is empty.

As well as the log of all tests and failed tests, test failures are added to the trace output. If you run the example as it stands, both of our complex number tests should fail, because our Add method is wrong. Correct it:

	public static function add(c1:Complex, c2:Complex) : Complex { 
		return new Complex(c1._r + c2._r, c1._i + c2._i);
	}

... and restart the application; this time they should pass. We now have unit tests defined and running, and can start testing more rigorously!

Running Tasks without the UI

The test interface may not always be convenient. It wraps an instance of TestRunner, which you can create directly. The test runner expects an instance of TaskManager, from my background tasks library – unit tests are run in the background – which you can create in place if you don't need it for anything else, e.g. testRunner = new TestRunner(new TaskManager(50, stage)). You can then pass a list of classes to TestRunner.startRun, which will result in the test methods within those classes being executed. The second argument to startRun is any token by which you can identify the test run.

The test runner fires several events which you will want to attach handlers to:

III. Filling Out the Example

We can fill out the test cases for the rest of complex arithmetic, to provide a more realistic size of example:

import RedCorona.Tests.TestCase;
public class ComplexTests {
	[Test(5, -2, 0, 0, 5, -2)]
	[Test(5, -2, 3, 3, 8, 1)]
	public function add(r1:Number, i1:Number, r2:Number, i2:Number, r_ans:Number, i_ans:Number) : void {
		var c:Complex = Complex.add(new Complex(r1, i1), new Complex(r2, i2));
		TestCase.current.assertEqual(new Complex(r_ans, i_ans), c);
	}
	
	[Test(5, -2, 0, 0, 5, -2)]
	[Test(5, -2, 3, 3, 2, -3)]
	public function subtract(r1:Number, i1:Number, r2:Number, i2:Number, r_ans:Number, i_ans:Number) : void{
		var c:Complex = Complex.subtract(new Complex(r1, i1), new Complex(r2, i2));
		TestCase.current.assertEqual(new Complex(r_ans, i_ans), c);
	}
	
	[Test(-3, 2, 0, 0, 0, 0)]
	[Test(-3, 2, 1, 0, -3, 2)]
	[Test(-3, 2, 2, 0, -6, 4)]
	[Test(-3, 2, -3, -2, 13, 0)]
	[Test(-3, 2, 2, -1, -4, 7)]
	public function multiply(r1:Number, i1:Number, r2:Number, i2:Number, r_ans:Number, i_ans:Number) : void{
		var c:Complex = Complex.multiply(new Complex(r1, i1), new Complex(r2, i2));
		TestCase.current.assertEqual(new Complex(r_ans, i_ans), c);
	}

	[Test(-3, 2, -3, 2, 1, 0)]
	[Test(-6, 4, -3, 2, 2, 0)]
	[Test(13, 0, -3, 2, -3, -2)]
	[Test(-4, 7, -3, 2, 2, -1)]
	public function divide(r1:Number, i1:Number, r2:Number, i2:Number, r_ans:Number, i_ans:Number) : void{
		var c:Complex = Complex.divide(new Complex(r1, i1), new Complex(r2, i2));
		TestCase.current.assertEqual(new Complex(r_ans, i_ans), c);
	}
	
	[Test(1, 0, 1, 0)]
	[Test(1, 1, 1, -1)]
	[Test(5, -3, 5, 3)]
	public function conjugate(r:Number, i:Number, r_ans:Number, i_ans:Number) : void {
		var c:Complex = new Complex(i, r).conjugate;
		TestCase.current.assertEqual(new Complex(r_ans, i_ans), c);
	}
}

Note how we've defined one test function for each piece of behaviour we want to make sure we've done right, and one test case for each type of condition within that behaviour. For example for multiplication there is a test for the zero case, for identity, for a scalar multiplication and a complex multiplication.

As an exercise you can break out the maths textbook and implement the functionality so that the tests pass!

IV. Behind the Scenes

The TestRunner is the class that manages and executes tests. For each class that you ask it to look at, it adds a marker to the task queue for the CLASS_START event, and then inspects each method within that class for [Test] metadata. flash.utils.describeType provides all the information we need, and the XML traversal built in to AS3 means it's simple to collect the test methods within the class:

var xml:XML = describeType(c);
var testMethods:Vector.<*> = new Vector.<*>();
for each(var method:XML in xml.factory.method) {
	var metadata:XMLList = method.metadata.(@name == "Test");
	if(metadata.length() > 0){
		testMethods.push({xml:method, metadata:metadata, host:c});
	}
}

Each method is then executed as a step in a LoopTask, and inside that step function, each instance of the [Test] metadata is executed as a test case. First any parameters from this case are extracted, then the test method is called inside a try/catch (otherwise a test case which threw an exception would stop the whole test execution), and finally a TEST_COMPLETE event is dispatched with the status of the current case.

Test assertions are handled by TestCase, which contains equality assertion helper functions; if the two objects passed to assertEqual aren't equal, fail is called, which sets the status of that test case to failed, which the listening code (such as the UI) can check for.

V. Download

This package contains the test runner code, the test interface, and the UI components that the interface requires. You'll need my background task library as well.

By downloading the library, you agree to the following licence conditions: (i) you will not claim this code as your own; (ii) you will not attempt to sell the code; and (iii) if anything goes wrong as a result of using this code, I won't be held responsible. Download AS3 implementation.