Introduction

You are currently browsing the weblog archives for January, 2005.

Archive for January, 2005

Tuesday, January 4th, 2005

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.

Saturday, January 1st, 2005

Unit Testing: Mock Objects Framework

Unit testing means testing a class in isolation. To be able to do this any collaborator must at least be replaced by some kind of stub implementation of it. While this is definitely a good thing and appropriate in some cases it is really awkward if you have many collaborators and if these collaborators have a rather large number of methods.
Another problem with stubs is that they are not flexible enough to meet every use case, especially the special ones that hardly ever occur (except you spent hours programming the stub).
Thus you need a more general and dynamic approach.

The solution is: Mock Objects.

Mock objects also take unit testing a step further. You do not just create a stub that acts as if it were real, you also verify that the class under test collaborates with the mock object in the expected way. That means for example that the class under test calls certain methods with certain arguments on the mock object.
To work efficiently with mock objects you of course need a framework that gives you the ability to create mock objects and work with them easily.
This article is about such a framework that is based on the approach the EasyMock Framework uses. So, if you have ever worked with easymock (it’s for Java) you should have no problem working with this one. The basic workflow is as follows:
Create a mock control for a specific class or interface, get the mock object from it, set your expectations, set the behavior of the mock object, switch to replay state, use the mock object as if it were a normal instance of your class and verify if all expectations have been met.
While this sounds a little complicated and awkward at first sight it really is not. Just take a look at the following demonstration:

  1. import org.as2lib.test.mock.MockControl;
  2.  
  3. // create mock control for class MyClass
  4. var myMockControl:MockControl = new MockControl(MyClass);
  5. // receive the mock object (it is in record state)
  6. var myMock:MyClass = myMockControl.getMock();
  7. // expect a call to the setStringProperty-method with argument ‘myString’.
  8. myMock.setStringProperty("myString");
  9. // expect calls to the getStringProperty-method
  10. myMock.getStringProperty();
  11. // return ‘myString’ for the first two calls
  12. myMockControl.setReturnValue("myString", 2);
  13. // throw MyException for any further call
  14. myMockControl.setDefaultThrowable(new MyException());
  15. // switch to replay state
  16. myMockControl.replay();
  17.  
  18. // the class under test calls these methods on the mock
  19. myMock.setStringProperty("myString");
  20. myMock.getStringProperty();
  21. myMock.getStringProperty();
  22.  
  23. // verify that alle expectations have been met
  24. myMockControl.verify();

If any expectation has not been met an AssertionFailedError gets thrown that explains what went wrong. In the above case no AssertionFailedError should be thrown because every expectation has been met but it hadn’t if we had called the getStringProperty-method only once on the myMock-mock.

There is much more that can be done with the framework but just refer to the api documentation that is included in the download (I generated it with the old version of as2api because the new does not work and it is thus a little complex. It may be clearer to open the as-file directly and take a look at the documentation there.).

Download the Mock Objects Framework for Flash (It’s actually a snapshot of the as2lib from the 01.01.2005. The mock objects framework is contained in the org.as2lib.test.mock package).