Introduction

Flash is a lightweight cross-platform runtime for rich media, enterprise applications and mobile applications, as well as an integrated development environment. Flash can be programmed in ActionScript 1/2/3.

Tuesday, January 4th, 2005 at 4:59 pm

Test Driven Development (TDD) and Unit Testing with Mock Objects in Practice

When your boss confronts you with a task you must handle, the first thing you do is thinking about it. You mediate on the task and try to find the best solution of doing it. You maybe draw some sketches to visualize the whole thing and get a clearer image.
But, what’s next? - Well, when practicing TDD the next thing (actually the first, but I’m not that pragmatic about it) is to write a test for the central class and methods. This helps a lot because, when writing a test you become aware of collaborators, you test all special cases and you do not introduce unnecessary complexity. You basically create the whole api and implementations by working with them, which leads to an easy to use api that is based on a practical ground.
That’s basically all about it.
You first write your tests for a method of a class, and then the actual method based on the test, until the test executes without causing errors. Then the test for the next method and so on. It’s an easy, intuitive (when you are used to it) and safe way of programming which leads to stable and easy to use code. Another bonus is that when you refactor your code, improve its performance or do some other changes you can always run the tests to see if everything is still working.
This article is supposed to be a practical one (at least that’s what the headline says), thus we will jump right into practicing TDD. All you need is the as2lib’s unit testing framework and the mock objects framework from the last article Unit Testing: Mock Objects Framework.
A request came from a bank that needs an application to do their banking transactions like transfering money onto an account. The account seems to be the central problem domain (btw. we are only going to build the domain model). The bank clerk must be able to book money to an account (credit) and to withdraw money from an account (debit). That said we can start writing our tests for the account’s constructor and the methods credit and debit. We will start with the constructor tests.

  1. import org.as2lib.test.unit.TestCase;
  2. import org.as2lib.test.mock.MockControl;
  3. import com.simonwacker.banking.Account;
  4.  
  5. class com.simonwacker.banking.AccountTest extends TestCase {
  6.  
  7.   public function testNew(Void):Void {
  8.     var account:Account = new Account();
  9.     assertSame(account.getBalance(), 0);
  10.   }
  11.  
  12. }

We want the account’s constructor to take no arguments and set the balance to zero. To be able to control that the balance is zero we need the ,getBalance’ method. But the compiler won’t compile. That’s because we have not created the ,Account’ class yet. That’s what we are going to do now, based on our first test.

  1. class com.simonwacker.banking.Account {
  2.  
  3.   public function Account(Void) {
  4.   }
  5.    
  6.   public function getBalance(Void):Number {
  7.     return 0;
  8.   }
  9.  
  10. }

The compiler does not complain anymore and when we execute the test everything seems to work.
To execute the test you need a fla with the following code in the first frame.

  1. import org.as2lib.test.unit.TestSuiteFactory;
  2.  
  3. com.simonwacker.banking.AccountTest;
  4. new TestSuiteFactory().collectAllTestCases().run();

It’s time to write the test for the ,credit’ method.

  1. public function testCreditWithNullAmount(Void):Void {
  2.   var account:Account = new Account();
  3.   account.credit(null);
  4.   assertSame(account.getBalance(), 0);
  5. }

The credit method takes one argument ,amount’. Thus the first thing to test is what happens if we pass-in an ,amount’ of value ,null’. I want the method to just do nothing. That means that after invoking this method the balance should still be zero.
To satisfy the compiler and the test we implement the ,credit’ method.

  1. public function credit(amount:Number):Void {
  2.   balance += amount;
  3. }

Everything works fine now and we are ready for the next test that passes an amount of 40.

  1. public function testCreditWithRealAmount(Void):Void {
  2.   var account:Account = new Account();
  3.   account.credit(40);
  4.   assertSame(account.getBalance(), 40);
  5. }

Executing the test fails. We need to do some major changes. To get it working we must be able to change the balance through the ,credit’ method dynamically.

  1. class com.simonwacker.banking.Account {
  2.  
  3.   private var balance:Number;
  4.  
  5.   public function Account(Void) {
  6.     balance = 0;
  7.   }
  8.  
  9.   public function credit(amount:Number):Void {
  10.     balance += amount;
  11.   }
  12.  
  13.   public function getBalance(Void):Number {
  14.     return balance;
  15.   }
  16.  
  17. }

When we run our test again we get the following output:

  1. ** InfoLevel **
  2. *** TestSuite <Generated TestSuite> (1 Tests) [5ms] ***
  3.   com.simonwacker.banking.AccountTest run in [5ms]. 1 error occured
  4.      testCreditWithNullAmount() [2ms] 1 error occured
  5.        assertSame failed!
  6.          NaN !== 0
  7.      
  8. *******************************************************

Out ,credit’ method does something wrong. It adds the amount to the balance even if it is ,null’. We have to prevent this.

  1. public function credit(amount:Number):Void {
  2.   if (amount != null) {
  3.     balance += amount;
  4.   }
  5. }

We enhance our ,testCreditWithRealAmount’ test and add a new one to be really really sure that everything works as expected.

  1. public function testCreditWithNotZeroBalanceAndNullAmount(Void):Void {
  2.   var account:Account = new Account();
  3.   account.credit(60);
  4.   account.credit(null);
  5.   assertSame(account.getBalance(), 60);
  6. }
  7.  
  8. public function testCreditWithRealAmount(Void):Void {
  9.   var account:Account = new Account();
  10.   account.credit(40);
  11.   assertSame(account.getBalance(), 40);
  12.   account.credit(30);
  13.   assertSame(account.getBalance(), 70);
  14. }

A business rule prescribes that the boss of the bank shall get notified if someone credits an amount that’s bigger or equal than 1 000 000 dollars to thank this guy personally. That notification is an email to the boss.
There already exists support for sending emails. This support is based on the ,EmailSender’ interface.

  1. interface com.simonwacker.banking.EmailSender {
  2.  
  3.   public function sendEmail(message:String):Void;
  4.  
  5. }

We write a test for the ,credit’ method in which we verify that the email sender’s ,sendEmail’ method gets executed when we credit an amount bigger than 1 000 000 dollar.

  1. public function testCreditWithAmountBiggerThanOneMillionDollars(Void):Void {
  2.   var emailSenderControl:MockControl = new MockControl(EmailSender);
  3.   var emailSender:EmailSender = emailSenderControl.getMock();
  4.   emailSender.sendEmail("Someone transfered 1 000 000 dollars.");
  5.   emailSenderControl.replay();
  6.  
  7.   var account:Account = new Account();
  8.   account.setEmailSender(emailSender);
  9.   account.credit(1500000);
  10.  
  11.   emailSenderControl.verify();
  12. }
  13.  
  14. public function testCreditWithAmountEqualToOneMillionDollars(Void):Void {
  15.   var emailSenderControl:MockControl = new MockControl(EmailSender);
  16.   var emailSender:EmailSender = emailSenderControl.getMock();
  17.   emailSender.sendEmail("Someone transfered 1 000 000 dollars.");
  18.   emailSenderControl.replay();
  19.  
  20.   var account:Account = new Account();
  21.   account.setEmailSender(emailSender);
  22.   account.credit(1000000);
  23.  
  24.   emailSenderControl.verify();
  25. }
  26.  
  27. public function testCreditWithAmountLessThanMillionDollars(Void):Void {
  28.   var emailSenderControl:MockControl = new MockControl(EmailSender);
  29.   var emailSender:EmailSender = emailSenderControl.getMock();
  30.   emailSenderControl.replay();
  31.  
  32.   var account:Account = new Account();
  33.   account.setEmailSender(emailSender);
  34.   account.credit(500000);
  35.  
  36.   emailSenderControl.verify();
  37. }

Because we are trained now we can make bigger steps and implement mutliple tests at once. The compiler complains because we have no method called ,setEmailSender’. Thus we introduce this method first.

  1. import com.simonwacker.banking.EmailSender;
  2.  
  3. class com.simonwacker.banking.Account {
  4.  
  5.   private var balance:Number;
  6.   private var emailSender:EmailSender;
  7.  
  8.   public function Account(Void) {
  9.     balance = 0;
  10.   }
  11.  
  12.   public function setEmailSender(emailSender:EmailSender):Void {
  13.     this.emailSender = emailSender;
  14.   }
  15.  
  16.   public function credit(amount:Number):Void {
  17.     if (amount != null) {
  18.       balance += amount;
  19.     }
  20.   }
  21.  
  22.   public function getBalance(Void):Number {
  23.     return balance;
  24.   }
  25.  
  26. }

When we run the test again we get the following output:

  1. ** InfoLevel **
  2. *** TestSuite <Generated TestSuite> (1 Tests) [26ms] ***
  3.   com.simonwacker.banking.AccountTest run in [26ms]. 2 errors occured
  4.      testCreditWithAmountBiggerThanOneMillionDollars() [11ms] 1 error occured
  5.        com.simonwacker.banking.AccountTest.testCreditWithAmountBiggerThanOneMillionDollars() threw a unexpected exception.
  6.          Expectation failure on verify:
  7.            sendEmail(Someone transfered 1 000 000 dollars.): expected: 1, actual: 0
  8.      
  9.      testCreditWithAmountEqualToOneMillionDollars() [5ms] 1 error occured
  10.        com.simonwacker.banking.AccountTest.testCreditWithAmountEqualToOneMillionDollars() threw a unexpected exception.
  11.          Expectation failure on verify:
  12.            sendEmail(Someone transfered 1 000 000 dollars.): expected: 1, actual: 0
  13.      
  14. ********************************************************

There’s still the sending functionality missing. We add it.

  1. public function credit(amount:Number):Void {
  2.   if (amount != null) {
  3.     if (amount >= 1000000) {
  4.       emailSender.sendEmail("Someone transfered 1 000 000 dollars.");
  5.     }
  6.     balance += amount;
  7.   }
  8. }

Executing the tests causes no errors now.

  1. ** InfoLevel **
  2. *** TestSuite <Generated TestSuite> (1 Tests) [18ms] ***
  3.   com.simonwacker.banking.AccountTest run in [18ms]. no error occured
  4. ********************************************************

That’s it. ;)
But there are still some things that we have not considered yet. What is if an email sender of value ,null’ gets set? What is if someone ,credits’ a negative amount?

In real life as you get more experienced writing tests and writing code you will just skip some of the little steps and combine them.

Download the sample code for Unit Testing with Mock Objects in Practice and Test Driven Development (TDD).
Download the as2lib snapshot from the 01.01.2005 which contains the unit testing and mock objects frameworks.