25.7.09

AS3: Basic Testing With FUnit in FlashDevelop

Basic Testing With FUnit

Testing is an often overlooked, but important part, of developing reliable applications (in any language). In this tutorial I will cover a very basic setup of FUnit in the FlashDevelop environment. I decided to make this blog because I couldn't find a reliable tutorial for the version of FUnit (0.70.0410), probably because it is in alpha stages.

What You Need:

  • FUnit: You can find it here. As of the time I'm writing this it is still in alpha. The FAQ page does say it is generally follows NUnit for structuring, so hopefully this tutorial will remain useful in the future versions.
  • FlashDevelop and Related Files: If you don't have FlashDevelop set up for programming yet, check out this tutorial.


Purpose of Unit Testing:

The goal of testing is to guarantee the accuracy of your code. The basic idea is that you pass known arguments to each of your methods, and compare values to the output and affected objects to see if each method behaves correctly.

FUnit Classes:

The basic classes of FUnit that will be dealt with in this tutorial are:

  1. TestSuite - This will load the tests in your classes in order to put them together so the TestRunner can run them.
  2. TestRunner - This is the class that actually runs your tests and returns the output. There are a few types of TestRunner's, but the only one that will be dealt with in this tutorial is "funit.core.TestRunner"
  3. DebugTestListener - This class gives a readable debug output of your test results. There are other graphical TestRunners that could eliminate the need to use this class, but as far as I can tell the graphical output is under heavy development and I don't want this tutorial to be outdated a day after I finish it.

User Created Classes:

  1. Main - The Main class created by FlashDevelop when you make a new AS3 project.
  2. ProjectTestSuite - This class will create a collection of all tests to be run by the TestRunner.
  3. BasicClass - This class will have a method that does a basic operation which we will test.
  4. BasicClassTest - This class will contain the tests for BasicClass.

Preparing the Project:

  1. Setup the project properties so that the debug output will display correctly. This may not always be necessary, but with FUnit 0.70.0410 and FlashDevelop 3.0.2 RTM it is. For some reason when they movie is loaded as a document in FlashDevelop the debug output never happens, so you have to change it to play externally. To change how the movie is played, click Project > Properties..., set Platform target to Flash Player 10, and change the "Test Movie" dropdown menu to "Play in external player".
  2. In order to use FUnit, FUnit.swc needs to be placed somewhere in the project (I put it in lib\FUnit). Next in FlashDevelop Project window, right click FUnit.swc and select "Add to Library". Now all of the classes of FUnit should be available in your project.
Making a Test Suite:

In my opinion, the easiest way to make sure all of your tests get run, is to make a Test Suite of your own.

  package
{
import funit.core.TestSuite;

public class ProjectTestSuite extends TestSuite
{

public function ProjectTestSuite()
{
// Add the Basic Test Class tests to this suite.
add(BasicClassTest);
}

}

}

To create the TestSuite, you simply create a class that extends TestSuite. In order to specify which tests the suite contains, make calls to add() with all of the classes containing tests as arguments.

Creating The Test Class:
Ideally unit tests should be created before the classes they test. In theory, creating tests that guarantee your program is functionally sound, and then passing them all by coding the program should create a functionally sound program. If you code first it's much easier to accidentally create code that is difficult to test, or tests that use knowledge about how the code works, which doesn't neccessarily check functionality.

The class to be tested in this tutorial will have only 2 methods. add(a:int, b:int):int and getLastResult():int. add() will add two numbers, store their result in a private class var, and return the result. getLastResult() will return the last result determined by the object.

Some points to consider about how these methods will work:

  1. If getLastResult() is called before add() is called for the first time, it throws an error.
  2. After a call to add(), the number returned, getLastResult() and the sum of the arguments added, should all be equal.
  3. After a second call to add() getLastResult() should change to reflect the result of the second call.

There are definitely more thing that could be tested for in this class, but I feel like these three major point should take away most of the chance for problems to occur. Each of these seperate situations should be its own test, resulting in the following code:


 package
{

import funit.framework.Assert;

[TestFixture]
public class BasicClassTest
{

// This object will be used by each test method
private var testObject:BasicClass;

public function BasicClassTest()
{

}

/**
* This method will reset testObject to the new
* instance each time a new test is run.
*/
[SetUp]
public function setup() : void
{
testObject = new BasicClass();
}

/**
* Tests calling getLastResult() before there is
* any result.
*/
[Test]
[ExpectedError("Error")]
public function testadd_error() : void
{
// Make an illegal call to getLastResult()
testObject.getLastResult();
}

/**
* Tests the functionality of add() and getLastResult()
*/
[Test]
public function testadd() : void
{
// Test the add and getLastResult() functions
Assert.areEqual(10, testObject.add(3, 7));
Assert.areEqual(10, testObject.getLastResult());
}

/**
* Tests the functionality of add() and getLastResult()
*/
[Test]
public function testadd_twice() : void
{
// Test the add and getLastResult() functions
testObject.add(3, 7);
Assert.areEqual(17, testObject.add(8, 9));
Assert.areEqual(17, testObject.getLastResult());
}
}

}

FUnit code line by line:

  • 6. [TestFixture] is metadata that FUnit uses to process this class (essentially as if it were extending the class TestFixture). It is necessary on classes like this, intended to be included in a suite.
  • 11. An empty instance of our class to be tested, this is used later in the code.
  • 22. [SetUp] This metadata tells the TestRunner to run this method before each test is run. The running order would be setup() ... test ... setup() ... test ... setup() ... etc. It will be used for each test in this class.
  • 25. testObject = new BasicClass() Before each test is run, this will place a new instance of BasicClass in testObject. This is useful because it means you won't have to create BasicClass individually somewhere in each of your tests. A note about using private members in tests: It is bad coding to have tests that rely on the results of other tests. This is importantly because many testing api's do not guarantee the order your tests will be run in.
  • 32. [Test] This tells FUnit that this method is a Test method. You will see it above each method that is used to test.
  • 33. [ExpectedError("Error")] This means that we expect an error of class Error to occur during this method call. If an error does not occur, then the test will fail.
  • 37. testObject.getLastResult() This is an illegal call because add() has not been called previously on the testObject instance. This should cause an error to be thrown and the test to pass.
  • 47. Assert.areEquals(10, testObject.add(3, 7)) This is the heart of most unit testing. It checks the equality of the number 10, and the value returned from the add function. If they are equal then the test passes, otherwise it fails. Assert contains a number of other ways to compare objects and primitives that can all be viewed in the FUnit documentation.
  • 48. This line guarantees that the getLastResult() function returns correctly after add() has been called.
  • 59. This line calls add() for the second time in this method.
  • 60. This line guarantees that the value of getLastResult() has been correctly updated after the second call to add().

Creating the class:

Now that the test is coded, the defined behavior can be used to write the actual class. There is nothing special about this class so I am just going to post it assuming that if you are dealing with testing, you understand AS3 well enough to make a simple class:


 package
{

public class BasicClass
{

private var lastResult:int = 0;
private var initialized:Boolean = false;

public function BasicClass()
{

}

/**
* Adds two numbers and puts the result in lastResult
* @param a number 1
* @param b number 2
* @return a + b
*/
public function add(a:int, b:int):int
{
// add() has been called so set initialized to true
initialized = true;
// Save the sum
lastResult = a + b;
// Return the sum
return lastResult;
}

/**
* Get property
*
* @return The last sum calculated by this instance.
*/
public function getLastResult():int
{
// If add() has not been called, throw an error
if (initialized != true)
{
throw new Error("An addition has not previously occurred");
}
// Otherwise return the last sum
return lastResult;
}

}

}

Get it running:
All that is left is to start up the TestRunner in the Main class and run the movie in debug mode. Following is the line by line:

 package
{
import flash.display.Sprite;
import flash.events.Event;
import funit.core.TestRunner;
import funit.listeners.automation.DebugTestListener;

public class Main extends Sprite
{

public function Main():void
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}

private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point
// Run the tests
runTests();
}

private function runTests():void
{
// Create a new TestRunner which will run the tests
var runner:TestRunner = new TestRunner();
// Create an instance of our test suite
var suite:ProjectTestSuite = new ProjectTestSuite();
// Load the suite into the runner
runner.load(suite);
// Run the loaded suite and output the results to Debug
runner.run( new DebugTestListener() );
}

}

}
  • 1-23. All standard FlashDevelop AS3 loading stuff, check here if you don't understand it.
  • 28. Create a new TestRunner (The class that actually runs the tests)
  • 30. Create an instance of the suite we created.
  • 32. Load our suite (Which thereby loads all tests added to the suite) into the test runner.
  • 34. Run the tests. The argument "DebugTestListener()" is an FUnit class which causes the results of the test to be shown in the Debug Console. For information on other forms of output, read the updates on the FUnit site.

Hopefully you've got everything working... if not, the source for this project can be found here.

Feel free to contact me with any comments or suggestions.