12/10/2008

My TDD process with FlexUnit


Some people have asked me how my TDD (Test Driven Development) flow is working with FlexUnit, so I decided to write an entry about it and explaining how´s and why´s.

First of all we have to know what to test, this is, the logic of the application, and we need to have a design that allows us to isolate this part of the application from the rest (the classic MVC). Cairngorm microarchitecture suits this pretty well, specially if we combine it with the presentation model pattern. For this purpose, I am not going to include Cairngorm for the good of simplicity.

We are going to create an application that gets 3 numbers and adds them (the simpliest the better).

The first thing you have to do when using TDD is write the tests for the logic you need; it might sound a bit weird the first time you do it, as you might ask yourself "how am I going to test something that doesn´t exist?". You don´t actually have to "test it", but if you know what you need to do then you can write the tests to verify that whatever you need works fine.

STEP ONE - Create the FlexUnit Project

This is extremely simple, yo just download and add to your Flex project the FlexUnit library and then create a new Application including the TestUnitBase component, just like this (TestRunner):

TestRunner.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:flexui="flexunit.flexui.*"
layout="absolute"
creationComplete="onCreationComplete()"
>

<mx:Script>
<![CDATA[

private function onCreationComplete() : void
{
}
]]>
</mx:Script>

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

STEP TWO - Write the tests class and add it to the new TestRunner application

We will need a class with a method that gets 3 numbers and returns the result of adding them, we will call this class NumberAdder, so we need to create a NumberAdderTest class to test all the public methods on it.

NumberAdderTest.as
package test.pascualin.testexample.number
{
import flexunit.framework.TestCase;

import pascualin.testexample.number.INumberAdder;
import pascualin.testexample.number.NumberAdder;

public class NumberAdderTest extends TestCase
{
private var model : INumberAdder;

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

override public function setUp():void
{
model = new NumberAdder();
}

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

public function testAddThreeNumbers() : void
{
assertEquals( "The adder failed", 4, model.addThreeNumbers( 1, 1, 2) );
assertEquals( "The adder failed", 5, model.addThreeNumbers( 1, 2, 2) );
assertEquals( "The adder failed", 6, model.addThreeNumbers( 2, 2, 2) );
assertEquals( "The adder failed", 7, model.addThreeNumbers( 3, 2, 2) );
assertEquals( "The adder failed", 8, model.addThreeNumbers( 3, 3, 2) );
assertEquals( "The adder failed", 9, model.addThreeNumbers( 3, 3, 3) );
}
}
}


A note on this file: I normally create an interface (INumberAdder) to represent he Model class (NumberAdder), this helps me in two different aspects, the first one is adding a new isolation layer on the project architecture, and the second one is a better design when adding new methods and properties on my NumberAdder class, as I will only make public those that really need to be accessed from outside (the ones that need to be tested), and all the other methods and properties I need to create when building the logic will be private.

Add the test class we have created to the full list of tests that will be added to the TestRunner.

AllTests.as
package test
{
import flexunit.framework.TestSuite;

import test.pascualin.testexample.number.NumberAdderTest;

public class AllTests extends TestSuite
{
public static function suite() : TestSuite
{
var testSuite : TestSuite = new TestSuite();

testSuite.addTestSuite( NumberAdderTest );

return testSuite;

}
}
}
Add the tests to the TestRunner on the creationComplete event handler:

TestRunner.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:flexui="flexunit.flexui.*"
layout="absolute"
creationComplete="onCreationComplete()"
>

<mx:Script>
<![CDATA[
import test.AllTests;

private function onCreationComplete() : void
{
testRunner.test = AllTests.suite();
testRunner.startTest();
}
]]>
</mx:Script>

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


STEP THREE - Create the Logic

NumberAdder.as
package pascualin.testexample.number
{
public class NumberAdder implements INumberAdder
{
public function NumberAdder()
{
}

public function addThreeNumbers( numOne : int, numTwo : int, numThree : int ) : Number
{
return numOne + numTwo;
}
}
}
Note: I have included an error on this logic so we can see out test failing here.

STEP FOUR - Correct your logic (not always needed :-P)

Change the NumberAdder to do what it´s really supposed to do:

NumberAdder.as
package pascualin.testexample.number
{
public class NumberAdder implements INumberAdder
{
public function NumberAdder()
{
}

public function addThreeNumbers( numOne : int, numTwo : int, numThree : int ) : Number
{
return numOne + numTwo + numThree;
}
}
}


And see the test passing here.

STEP FIVE - Build the view and link it to the logic

Once we have verified that our logic works as spected, we can just build the view we want and link it to the logic, in this example I will use NumericSteppers to send the numbers to the NumberAdder class

TDDExample.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
xmlns:comp="pascualin.testexample.comp.*"
creationComplete="onCreationComplete()"
>
<mx:Script>
<![CDATA[
import pascualin.testexample.number.NumberAdder;

private function onCreationComplete() : void
{
numberAdderView.model = new NumberAdder();
}
]]>
</mx:Script>

<comp:NumberAdderView
id="numberAdderView"
width="700"
/>
</mx:Application>
NumberAdderView.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml"
>

<mx:Script>
<![CDATA[
import mx.controls.Alert;
import pascualin.testexample.number.INumberAdder;

[Bindable]
private var result : String;

public var model : INumberAdder;

private function onAddNumbers() : void
{
txtResult.text = model.addThreeNumbers( num1.value, num2.value, num3.value ).toString();
}
]]>
</mx:Script>
<mx:NumericStepper
id="num1"
/>
<mx:NumericStepper
id="num2"
/>
<mx:NumericStepper
id="num3"
/>
<mx:Button
label="Add them!"
click="onAddNumbers()"
/>
<mx:Label
text="result:"
/>
<mx:Text
id="txtResult"
text="{ result }"
/>
</mx:HBox>


And we know that if we have any problem it´s probably going to be view-related, this will make it much easier to find and solve :)

Application Running:



I hope it helps ;)

4 comments:

  1. Testing models like this is really easy, but how do you test the complex stuff (which needs the most testing): the views?

    ReplyDelete
  2. Hi anonymous, the view should always be controlled by a presentation model, and thats the way you test the view, for example, if you have a ViewStack and you need to verify if the selectedIndex is correct, you would bind that property to a Presentation Model property, and then in your test you will check that this property has the correct value.

    If you are still having problems wth testing the view behaviour you should probably have another thought about your design.

    Hope it helps ;)

    ReplyDelete
  3. Debería dar errores de compilación poner los paréntesis de ese modo, jajajaja.

    It's a joke, mate. I have readed all and I'm using in my project, so thanks :)

    I think the more interesting thing here is using the test as a way to develop you functionalities, that's is TDD.

    ReplyDelete
  4. Hi Jose Ignacio,

    The porpoises of TDD are basically 3:

    1- Use it in order to develop (as you commented)
    2- A way of being sure that the code you develop does what it is suppose to do
    3- Use the Tests as an API for your methods, so any new developer joining the project can tell what a method does and how to use it just by looking at its tests. This way comments on the code are very rarely needed :)

    Good luck with TDD on your project, let us know how you get on with it!

    ReplyDelete