If you had declared your namespace as "bananas", you would instantiate your component using code similar to:
<bananas:TestRunnerBase id="testRunner" />
But you've used common sense and gone for a more descriptive name.
The TestRunnerBase component is TestRunner (see the "Understanding Terminology" section, above, for a refresher). I assigned it an id of testRunner
.
In the Application tag you also have the creationComplete
event handler, which callsonCreationComplete()
. This method sets the test suite(s) you are going to use, and then calls the startTest()
method of the TestRunner to put everything in to action.
Finally, underneath the script block you instantiate our TestRunner as previously described, with a width and height of 100%.
Okay, that's it. You'll be glad to hear that you don't really need to touch this file again in this tutorial.
Creating AccountTest.as:
Create a new ActionScript file in the main project folder (at the same level as main.xml) called AccountTest.as, and paste the following code into it:
package
{
import flexunit.framework.TestCase;
import flexunit.framework.TestSuite;
public class AccountTest extends TestCase
{
public function AccountTest(methodName : String){
super(methodName);
}
public static function suite():TestSuite{
var accountTS:TestSuite = new TestSuite();
//tests are added to the suite here
return accountTS;
}
} }
There are a few things to notice:
- The class extends TestCase.
- There is one static function called suite (which returns a TestSuite).
- The constructor calls its super class, passing it some method name.
Creating Account.as
Finally, you must create the Account
class. Create a new ActionScript file in the main project folder (at the same level as main.xml and AccountTest.as) called Account.as and paste the following code into it:
package
{
public class Account
{
public function Account(){
}
}
}
Not much to say about this. It is the bare bones of the actual class.
WRITING THE FIRST TEST
I'm going to plagiarize (with permission) the example used by Simon Whacker to demonstrate as2lib's unit testing, because it was particularly clear and easy to understand.
We are going to create an application for a bank, and our first requirement is to allow the creation of a new account.
The first question to ask yourself is: When creating a new account, what should happen? Well, not much. Despite my best wishes, whenever I open a new account, it always starts off empty. How do I know that for sure? Well. I know because I can check my balance. So, when I open a new account and check my balance I should have £0.00 in there. Okay, we have our first test.
Remember, in this tutorial, we write our test before we write the code in our class, and this is how it works:
In AccountTest.as, underneath our static "suite" method, paste in this code:
public function testNew():void{
var account:Account = new Account();
assertEquals("Expecting zero account balance", 0, account.getBalance());
}
Note: In some frameworks all tests must start with the prefix "test." With FlexUnit, this rule doesn't seem to be enforced, but for consistency I recommend doing so anyway. One reason to require any test method to start with "test" is that you can then shortcut the suite creation process. Simply introspect a class and any function that starts with "test" is considered a test.
In this case, you are testing the creation of a new account and so the method is called testNew
.
As you can see, you create a new instance of the Account
class, and then use an assertion method called assertEquals.
There are various types of assertion, but assertEquals
basically checks that the two values passed in are ... [drum roll] ... equal. The eagle eyed among you may have noticed I am actually passing in three values:
- The first value is optional. It is a String and it should clarify the outcome you are looking to satisfy. In this case: "Expecting zero account balance."
- The next value is 0.
- The last value is a call to an, as yet non-existent, method of our
Account
class calledgetBalance().
I will write this in a moment.
Overall, this assert specifies that you want the value returned from getBalance()
to equal zero.
Note: TestCase
extends Assert, so there is no need to import the Assert class in order to use these assertion methods.
All you have to do now is add your test to the test suite.
Amend the static suite function in the AccountTest
class to look like this:
public static function suite():TestSuite{
var accountTS:TestSuite = new TestSuite();
accountTS.addTest(new AccountTest("testNew"));
return accountTS;
}
You can see from the above code that I use the addTest
method to add the test to the TestSuite. The object passed in is actually an instance of AccountTest
(in other words, the class you are in). The name of the test to run, gets passed in as a String...
If you remember, the constructor for AccountTest looks like the following code:
public function AccountTest(methodName : String){
super(methodName);
}
It accepts the methodName
of the test, and passes it on to its super class.
Note: This is the way I first learned to test, but you don't have to do it like this. The framework will automatically find all "test" methods in a TestCase if you pass the class reference to the constructor of a TestSuite.
Test It!
Okay, the setup is done. It's time to test.
If you think back to the Test-Code-Simplify cycle of eXtreme Programming that we looked at earlier. The first couple of stages were:
- Write a single test.
- Compile it. It shouldn't compile, because you haven't written the implementation code it calls
You've written a test, so try and compile the project.
Figure 1. The Errors window
The Flex Builder Problems pane should also be reporting the following error:
"Call to a possibly undefined method getBalance through a reference with static type Account."
Great! That's what we want. You haven't written getBalance()
yet And so you should quite rightly have a compiler error.
Note: If you actually choose to ignore the error and run the application anyway, you will see the TestRunner, and the test will actually pass! I didn't expect this to happen so be aware of it. It is fine to ignore Flex Builder warnings (in fact, the kind of tests you'll do will probably cause a few warnings) but don't ignore compile-time errors.
From further research, I found out that the test passes based on the last time you saved. So if you saved an empty test method, it would work; or, if you saved the empty testcase it would run and report no failures. When there are compiler errors, a new SWF file doesn't replace the old SWF, so you are in fact running the old SWF file.
Now take a look at the next two steps in the Test-Code-Simplify cycle:
- Implement just enough code to get the test to compile.
- Run the test and see it fail.
Amend the Account
class to look like this (additions are highlighted in yellow):
package
{
public class Account
{
private var _balance:Number;
public function Account(){
}
public function getBalance():Number{
return _balance;
}
}
}
You now added your getBalance()
method, which returns _balance
, and you have also declared this variable called _balance
(intended to hold our account balance amount of course), but _balance
hasn't been initialized with a value yet. Compile, and you should see something like the following:
Figure 2. The TestRunner GUI
This error is a good thing! Here we can see the test that failed (testNew) and why it failed. The big red bar would be green if the test had passed.
Now you know your TestRunner is working properly. You know the test should have failed, you know your test did fail, and you know why it failed.
The TestRunner tells us that it expected a value of zero but received a value of NaN
(Not aNumber) which is what ActionScript 3.0 returns (variables typed as Number and not assigned a value are returned as NaN in ActionScript 3.0).
Continuing on, the next two steps in the Test-Code-Simplify cycle are:
- Implement just enough code to get the test to pass
- Run the test and see it pass
All you need to do now is give _balance
an initial value of zero. Change your Account class's constructor to look like the following:
public function Account(){
private var _balance:Number = 0;
}
Now run the test:
Figure 3. The test succeeds in the All Tests tab
Bingo. The bar is green. Here we are viewing the "All Tests" tab in the left pane, which shows that the testNew
test has passed. You know it passed for the right reasons too, because you saw it fail when conditions were wrong.
The final two points in the Test-Code-Simplify cycle are:
- Refactor for clarity and "once and only once"
- Repeat
There's little need to refactor here, and so on with the next test.
ONWARDS AND UPWARDS
You've completed your first test—the hard part is over with. Take a break if you're exhausted because in order to see the real value of TDD we are going to implement a couple of extra tests. Most of the following code will be copy and paste with little need for additional explanation, so it shouldn't take long to get to the end now
GIVE ME SOME CREDIT
In many ways this next part of the tutorial is the most important. I hope to demonstrate how TDD can help you spot mistakes and prevent you from making changes which could break existing code and leave you unaware. Catching these problems when they occur, can save hours of debugging and headache further down the line.
Let's say your bank customers can open an account and now they need a way to credit their accounts. I'm sure you can think of plenty of things you need to test for here. Just as an example:
- crediting an account with a null amount (avoid this!)
- crediting an account with a real value (must work!)
- crediting an account with a negative amount (that's a debit not a credit)
- an account credit which includes fractions of a penny (how to handle?)
We're not going to implement all of these, but we'll use a couple of them to highlight some points.
Credit with null value
This seems pretty straight forward so first let's write the test:
public function testCreditWithNullValue():void{
var account:Account = new Account();
account.credit(null);
assertEquals("Expecting zero account balance", 0, account.getBalance());
}
Again, you have called a method that doesn't exist yet credit()
, but in doing so you have effectively specified that it is required, and must be capable of accepting a value. This shows how TDD helps you write your specifications.
Now add your test to our test suite:
accountTS.addTest(new AccountTest("testCreditWithNullValue"));
Check that your application doesn't compile; sure enough, Flex Builder gives you a compile error because you haven't written the credit()
method yet.
Next, write just enough code in the Account class to let it compile.
public function credit(amount:Number):void{
_balance += amount;
}
Run the test and see it fail:
Only, this one doesn't fail.
Flex Builder warns is that AccountTest ('null' is being used where 'Number' is expected), but there is no error. In fact ActionScript 3.0 appears to handle the attempt at passing in a null and returns a balance of zero.
In ActionScript 2.0, adding null to zero would equal NaN (Not a Number), so let's quickly check that our assumption about this is correct by making the test fail on purpose.
Change the code as follows:
account.credit(null + 10);
When we run the test it does indeed fail, because it is expecting zero, but receives 10. It looks like the code is doing what you wanted, so remove the "+ 10" and write the next test.
Credit with real value
Now make sure you can credit the account with a real value.
Use the amount £12.34, although the currency is obviously irrelevant here.
Once again, create a test and add it to the test suite. I show the test as follows:
public function testCreditWithRealValue():void{ var account:Account = new Account(); account.credit(12.34); assertEquals("Expecting account balance with pounds and pence.", 12.34, account.getBalance()); }
Again, you create a new instance of the Account class, call the credit method (which now does exist) and pass in a value of 12.34. Therefore, you expect a balance of 12.34 returned.
Finally, this test passes for the first time. With something as simple as this (considering our other tests are also in place) I don't think you need to double check it by making it fail. On with the final test.
Credit to three decimal places (fractions of a penny)
In the next scenario, the bank has specified that it will allow account credits which include fractions of a penny, but it will always round these amounts down and keep the extra (they are a bank after all); so if you put in thirty seven and a half pence, the account would be credited with 37 pence. Here, you simply check a number to three decimal places.
Write the test (see below) and add it to the test suite (not shown):
public function testCreditWithRealValueTo3DecimalPlaces():void{ var account:Account = new Account(); account.credit(1.234); assertEquals("Expecting account balance of 1.23", 1.23, account.getBalance()); }
Run the application and see it fail. Good. There is enough code in place for the application to compile already, but of course you are expecting a rounded-down figure, and the current credit method is not set up for that, so you get the following TestRunner error:
Error: Expecting account balance of 1.23 - expected:<1.23> but was:<1.234>
That's exactly what is expected. Now you write enough code to make it pass, which means changing the credit()
method to round off the amount. Change it to look like the following (changes in highlight):
public function credit(amount:Number):void{ var rounded = Math.floor(amount * 100)/100; _balance += rounded; }
If you run the test, it passes. Notice that Flex Builder warns you that you forgot to give therounded variable a Type definition. To do that, change the code to:
var rounded:int = Math.floor(amount * 100)/100;
Run the application and...
Hang on a minute. We seem to have broken a couple of tests.
Let's look at the first one:
Figure 4. Running the testCreditWithRealValue
test
The testCreditWithRealValue
was passing before, so what did we do to break it? Well as you can see, the test was expecting a real value of 12.34 but it only received 12.
In a moment of distraction, we typed the variable incorrectly as int, and of course integers arewhole numbers. In this case my contrived example was pretty obvious, but it shows how changing or implementing new functionality can break earlier code, and how TDD alerts you to that fact instantly.
All you need to do to fix this particular error is to type our variable as Number:
rounded:Number = Math.floor(amount * 100)/100;
Now run the application and all tests pass.
Let's look at the testCreditWithRealValue
test again for a moment... luckily you chose to use decimals in the "real value" test. Obviously an integer is a valid "real value" too, and if you had just used an integer, you wouldn't have spotted this error so easily. It is important to spend a moment naming our tests accurately, and more importantly, ensuring that you can't break a test down further in to smaller tests. One of the XP maxims is "Test until fear turns to boredom". TDD is great, but it's not an instant solution to all your problems. I wanted to show both how useful it can be, and how thin the distinction can be between useful feedback and no feedback at all.