26
Aug/091
Unit Testing Flex – Mocking
If you have experimented with unit testing either recently or in the past you will be aware that sometimes it can get difficult. There are many challenges you might encounter including the following:
* You need to ensure something happened, without going into detail.
* You have to do too much to write a single test.
* You need to test something that you don't have control over.
In this post I want to show some ways to get around these common unit testing roadblocks using a concept called mock objects. Basically a mock object is just an object that can simulate the behaviour of another object in order to make unit tests easier to write.
Imagine yourself working as a developer at an exciting new tech startup...
Your company is launching a new website tomorrow that lets users advertise and sell their cars online. Your boss turns up at your desk at 10 am in the morning, he has a rather worried look on his face. It turns out that he forgot to schedule the feature that actually allows users to publish vehicles to the site. Oops! Your boss tells you to drop everything and get this done by the end of the day because if you don't, your fired!
You look through the code and find that a service method exists that does what you want:
package com.compact.mocking {
public class VehicleService {
public function publish(item:Vehicle): void {
...
}
}
}
Excellent now all we need to do is ensure we call this function from our GUI model.
package com.compact.mocking {
public class VehicleModel {
public var service:VehicleService;
public function publish(item:Vehicle): void {
service.publish(item);
}
}
}
Easy, next on the list is to write a unit test so we are 100% sure that we are calling the right function. This is were things get a little tricky, we know the VehicleService works and we don't need to test it again. All we really need to do is make sure that we call the vehicle service... but after that we don't really care what happens.
You need to ensure something happened, without going into detail.
Using mockito-flex we are going to create a mock version of our service. We will then replace the real service with the mock version. Thanks to mockito-flex our mock will automatically record all invocations it receives. We can then inspect these records to verify if the publish function has been called.
package com.compact.mocking {
import org.mockito.MockitoTestCase;
// MockitoTestCase extends the standard FlexUnit TestCase.
public class CustomerModelTest extends MockitoTestCase {
private var _model:VehicleModel;
public function CustomerModelTest() {
// Tell mockito which classes will need to be mocked.
super([VehicleService]);
}
override public function setUp(): void {
_model = new VehicleModel();
// Replace the real service with a mock version.
_model.service = mock(VehicleService);
}
public function testPublishDelegatesToService(): void {
var item:Vehicle = new Vehicle();
// This should call the publish function of our mock service.
_model.publish(item);
// Verify that the mock service received the right call.
verify().that(_model.service.publish(item));
}
}
}
Your boss is impressed, you finished ALL that within 5 minutes! Unfortunately the victory is short lived because he just remembered something... that's right you need make sure the vehicle is valid before publishing.
You have a look through the code and find that this validation method exists on the vehicle:
package com.compact.mocking {
[Bindable]
public class Vehicle {
public var make:String;
public var model:String;
public var price:Number;
public var ownerName:String;
public var locationName:String;
public function isValid(): Boolean {
var valid:Boolean = true;
valid = valid && notEmpty(make);
valid = valid && notEmpty(model);
valid = valid && notEmpty(ownerName);
valid = valid && notEmpty(locationName);
valid = valid && price > 0;
return valid;
}
private function notEmpty(value:String): Boolean {
return value && value.length > 0
}
}
}
So you change your model to take advantage of it.
package com.compact.mocking {
public class VehicleModel {
public var service:VehicleService;
public function publish(item:Vehicle): void {
if(item.isValid()) {
service.publish(item);
}
}
}
}
And then expertly update your test.
package com.compact.mocking {
import org.mockito.MockitoTestCase;
public class CustomerModelTest extends MockitoTestCase {
private var _model:VehicleModel;
public function CustomerModelTest() {
super([VehicleService]);
}
override public function setUp(): void {
_model = new VehicleModel();
_model.service = mock(VehicleService);
}
public function testPublishDelegatesToService(): void {
var item:Vehicle = new Vehicle();
item.make = "a";
item.model = "b"
item.price = 5;
item.ownerName = "fred";
item.locationName = "australia"
_model.publish(item);
verify().that(_model.service.publish(item));
}
}
}
You get the job done but you feel like you had to do a lot of work to get there. You also remember that your colleague Fred is currently working on adding more conditions to the validation function. That means you will need to add even more setup to this test.
You have to do too much to write a single test.
You wish there was a way to avoid having to do all that work to make your vehicle valid. Wouldn't it be great if you could just create a mock vehicle that is always valid? Well actually... you can!
package com.compact.mocking {
import org.mockito.MockitoTestCase;
public class CustomerModelTest extends MockitoTestCase {
private var _model:VehicleModel;
private var _item:Vehicle;
public function CustomerModelTest() {
// We are mocking the vehicle as well.
super([VehicleService, Vehicle]);
}
override public function setUp(): void {
_model = new VehicleModel();
_model.service = mock(VehicleService);
// Create the mock vehicle.
_item = mock(Vehicle);
}
public function testPublishDelegatesToService(): void {
// Update the mock so the isValid() function always return true.
given(_item.isValid()).willReturn(true);
_model.publish(_item);
verify().that(_model.service.publish(_item));
}
}
}
Your boss is definitely impressed now.. you just cut five lines out of that test and when Fred updates the validation function.. your test won't break!
Everything is going well as you get ready for the big release, it's 3pm and your just about to celebrate over a few drinks when your boss remembers something... You need to record the time that every vehicle was published, because you are planning on charging your users 50c for each day they have their vehicle on your website.
Amazingly it turns out this functionality is already there on the service!
package com.compact.mocking {
public class VehicleService {
public function publish(item:Vehicle): void {
...
}
public function recordPublishTime(item:Vehicle, time:Date): void {
...
}
}
}
No problem, we just need to add a call to this function in our model.
package com.compact.mocking {
public class VehicleModel {
public var service:VehicleService;
public function publish(item:Vehicle): void {
if(item.isValid()) {
service.publish(item);
service.recordPublishTime(item, new Date());
}
}
}
}
Ok so that's looking good, except how can we verify this call using mockito?
A first attempt might look like this:
public function testPublishRecordsPublishTimeUsingService(): void {
given(_item.isValid()).willReturn(true);
_model.publish(_item);
verify().that(_model.service.recordPublishTime(_item, new Date()));
}
Unfortunately that won't work. The date we are using in our verify is not the same as the date we are creating in our model because the two dates have different millisecond values. How do we take control of the date creation process so that we can match these dates and complete the test?
You need to test something that you don't have control over.
Fortunately it's not too hard, we just need to introduce a date factory. Once we have this factory, we can then use a mock to precisely control the date creation process. This is what the factory looks like.
package com.compact.mocking {
public class TimeFactory {
public function currentTime(): Date {
return new Date();
}
}
}
We then change our model to use the factory instead of directly creating dates.
package com.compact.mocking {
public class VehicleModel {
public var service:VehicleService;
public var timeFactory:TimeFactory = new TimeFactory();
public function publish(item:Vehicle): void {
if(item.isValid()) {
service.publish(item);
service.recordPublishTime(item, timeFactory.currentTime());
}
}
}
}
Now we can fairly easily mock the factory and complete the test.
package com.compact.mocking {
import org.mockito.MockitoTestCase;
public class CustomerModelTest extends MockitoTestCase {
private var _model:VehicleModel;
private var _item:Vehicle;
public function CustomerModelTest() {
super([VehicleService, Vehicle, TimeFactory]);
}
override public function setUp(): void {
_model = new VehicleModel();
_model.service = mock(VehicleService);
_model.timeFactory = mock(TimeFactory);
_item = mock(Vehicle);
}
public function testPublishRecordsPublishTimeUsingService(): void {
var publishDate:Date = new Date();
given(_item.isValid()).willReturn(true);
given(_model.timeFactory.currentTime()).willReturn(publishDate);
_model.publish(_item);
verify().that(_model.service.recordPublishTime(_item, publishDate));
}
}
}
Phew! You did it.
You just saved the company! You completed the feature that allows your customers to publish their vehicles onto your new website. You unit tested your code and did so in an elegant way using mock objects and the mockito-flex framework. You encountered three common problems on your journey and overcame them all. Now all you need to do is continue the good work you have been doing and try your hand writing more unit tests.
Filed under: flexunit, testing
Wednesday 24 March 2010
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment