Unit Testing Done Right with Mock Objects
- Posted by bcarr on 03.13.2010 10:27 AM | 12 comments
I'm willing to bet that any developer who has worked with unit testing in the past can attest to the unique sense of deep spiritual satisfaction that only a successful execution of a battery of unit tests can elicit. An inexorable early warning system for the soul. Geeky - absolutely, but this general sense of well-being is of course rooted in something very real and while it's no guarantee of complete functional correctness, it goes a long way to helping us sleep better at night knowing the software - you know, works.
The general intent of a unit test is to focus on a specific piece of functionality or "system under test" (SUT), isolate it from the rest of your software and assert whether or not it behaves as expected. And while this is a fairly straight-forward objective, in practice attaining the correct level of isolation can be difficult if not impossible without mock objects.
See, Here's the Problem..
In most object-oriented code bases, you're likely going to see objects with many composite dependencies - objects that rely on other objects in order to produce a specific behavior. For example, if Object A has a method named doSomething(), it's not unlikely to see Object A delegate to or otherwise implement Object B in order to produce a valid result for doSomething(). Object B could in turn rely on Object C to get it's job done - layers of abstraction can be deep in many instances. There are two key problems with unit testing the doSomething() method on a fully-constructed object instance;
- Establishing a unit test for such an object means you must ensure that each dependency (Object B and C) is in the correct state in order to produce the desired expectation of the doSomething() method - this means lots of additional code in our unit test simply to prepare for our assertion. This is also greatly affected by the actual complexity of the doSomething() method (number of linear paths of execution) AND complexity of each dependency - which can grow large, fast.
- It's not a unit test anymore - it's scope is no longer granular enough. It now exists withing the strange in-between of a unit and an integration test. Invoking doSomething() not only exercises that method, but also it's collaborators API's, their collaborators API's, etc..
Correct unit testing SUT isolation occurs when you can exercise behavior in a way where you do not have to establish the state of or rely upon the implementation of an object dependency (collaborator) in order to assert behavior on the SUT itself.
A Mock Object Is...
A "Mock" object is an object instance that mimics an actual concrete type. A shifty binary doppelganger at your very beck and call. Conceptually a mock object can be substituted in every way for it's actual concrete counterpart. For instance it can be provided as a valid argument to strongly-typed method parameters on other objects. What makes it a "mock" object is that you can explicitly indicate what a specific method will return regardless of it's implementation - this allows you to effectively short-circuit any collaborators that may be involved in producing a real result on an actual object. Clear as mud I'm sure - so let's get into some examples.
Set up
Although there are many different mock object library and unit testing providers, I'm going to stick with that which I'm most comfortable - MXUnit and MockBox. If you'd like to follow along with the examples, make sure you download each one and have them properly installed.
Let's Get Down to the Get Down
We're going to create a unit test for a User object named TestUser. The User object happens to be very domain-driven, meaning the User object itself responsible for data-member validation and persistence - even though it relies on a small number of dependencies to perform those tasks. It's basic design is as follows;
The UserValidator is responsible for ensuring the User instance is in a correct state prior to persisting it, if there are problems with the state of the object, each problem is registered with a structure as a string and returned. If the resulting structure is empty, then validation passed. The UserDAO is of course responsible for persisting the instance - if persistence fails, a false is returned, true otherwise.
Our User implementation;
component accessors="true" {
property string userName;
property string emailAddress;
property UserDAO dao;
property UserValidator validator;
property struct errors;
public User function init() {
return this;
}
public boolean function save() {
var errors = getValidator().validate(this);
if (structCount(local.errors)) {
variables.errors = local.errors;
return false;
}
if (!getDAO.save(this)) {
return false;
}
return true;
}
public boolean function delete() {
if (!getDAO.delete(this)) {
return false;
}
return true;
}
}
Isolate the System Under Test
The first step in proper SUT isolation is to identify it's collaborators, a.k.a. dependencies. In the case of our User object, there are two direct collaborators; UserValidator and UserDAO. Instances of these two types are required to perform the tasks save() and delete(). Let's create the shell of the MXUnit test;
component extends="mxunit.framework.TestCase" {
public void function setUp() {
// our system under test
user= new User();
}
}
I've declared our User instance inside of a setUp() ensuring that for each test we create within this TestCase, a fresh instance of User will be readily available to us.
Rule of Thumb: Dependency "Hows" Don't Matter - Just "Whats"
This is where the magic of mocking comes into play. We're going to create mock instances of each User dependency. We want mock objects instead of the actual ones because we're not testing dependencies - we're testing User - how each dependency performs its respective job is irrelevant, we simply care about what they're capable of returning as it relates to the implementation of User.
To be more specific, when we write our test for the save() method on User, we know that the UserValidator instance is used to help implement the behavior - we don't care how the validate() method works, we simply know that it's capable of a structure, populated or not and that the behavior of save() depends on that result.
Create Your Mock Dependencies / Collaborators
We're going to create a utility instance of the MockBox framework. The resulting object will provide an API that will allow us to create mock objects from our concrete dependencies. After the utility is instantiated, we create actual mock objects on lines 10 and 11 and then provide those dependencies to the User on lines 13 and 14;
component extends="mxunit.framework.TestCase" {
public void function setUp() {
// our system under test
user= new User();
// MockBox utility
mb = new coldbox.system.testing.MockBox();
validator = mb.createMock(object = new UserValidator());
dao = mb.createMock(object = new UserDAO());
user.setValidator(validator);
user.setDAO(dao);
}
}
Time to put this to use - let's first assert the first type of save() failure - this condition occurs when the validator returns a populated structure, indicating validation failed. MockBox makes establishing this expectation with the validate() method of the UserValidator very easy - per line 20;
component extends="mxunit.framework.TestCase" {
public void function setUp() {
// our system under test
user= new User();
// MockBox utility
mb = new coldbox.system.testing.MockBox();
validator = mb.createMock(object = new UserValidator());
dao = mb.createMock(object = new UserDAO());
user.setValidator(validator);
user.setDAO(dao);
}
public void function testSaveValidationFailure() {
// ensure the validator returns a populated structure
validator.$(method="validate", returns={error="doesn't matter"});
assertFalse(user.save());
}
}
Now when line 15 of User is executed - instead of invoking the actual validate() method - our mock kicks in, ignores the implementation of validate() on our UserValidator instance (remember - we don't care how it gets it's job done for this test) and simply returns what we told it to, in this case a structure with a single key named "error". We can do this because the original validate() method indicates it will return a structure - if we attempt to override the returntype in the returns method with any other data-type, an exception will be thrown. Our first test has been successfully and accurately run on a SUT that has been completely isolated from the rest of the software.
Where did that "$" come from?
When we asked the MockBox utility for a mock object via createMock(), that utility decorated our actual type with a whole host of new and useful behavior - not the least of which is a simple method whose name is nothing more than a dollar-sign. We've used this method to mock a method call on the selfsame object instance. In the case of the validator - we're mocking the returntype of validate().
Expressing a different condition is a simple matter of having the mock object return an empty structure, which is required in order for us to properly test the second linear path of execution through the save() method - but now that validation passes, we know that line 22 of User will be run - so we need to ensure now that the expectations of the DAO are set;
component extends="mxunit.framework.TestCase" {
public void function setUp() {
// our system under test
user= new User();
// MockBox utility
mb = new coldbox.system.testing.MockBox();
validator = mb.createMock(object = new UserValidator());
dao = mb.createMock(object = new UserDAO());
user.setValidator(validator);
user.setDAO(dao);
}
public void function testSaveValidationFailure() {
// ensure the validator returns a populated structure
validator.$(method="validate", returns={error="doesn't matter"});
assertFalse(user.save());
}
public void function testSavePersistenceFailure() {
// ensure the validator returns an empty structure
validator.$(method="validate", returns={});
dao.$(method="save", returns=false);
assertFalse(user.save());
}
}
To complete testing on the save() method, we need only to test for a resulting true, indicating success;
component extends="mxunit.framework.TestCase" {
public void function setUp() {
// our system under test
user= new User();
// MockBox utility
mb = new coldbox.system.testing.MockBox();
validator = mb.createMock(object = new UserValidator());
dao = mb.createMock(object = new UserDAO());
user.setValidator(validator);
user.setDAO(dao);
}
public void function testSaveValidationFailure() {
// ensure the validator returns a populated structure
validator.$(method="validate", returns={error="doesn't matter"});
assertFalse(user.save());
}
public void function testSavePersistenceFailure() {
// ensure the validator returns an empty structure
validator.$(method="validate", returns={});
dao.$(method="save", returns=false);
assertFalse(user.save());
}
public void function testSaveSuccess() {
// ensure the validator returns an empty structure
validator.$(method="validate", returns={});
dao.$(method="save", returns=true);
assertTrue(user.save());
}
}
Final Thoughts
Correct unit testing SUT isolation is crucial to good and accurate unit tests, especially in systems that exhibit a high degree of complexity. I highly recommend that you continue to explore the mocking framework options available to ColdFusion to see which is the best fit for you and your testing strategy. Until next time - happy testing!


Great article, Brian! Mocking it a topic close to my heart and I think you've done an excellent job explaining it.
I just wanted to add that the next version of MXUnit will have built-in mocking capabilities, which will support MockBox, as well as MightyMock, a new mocking framework from the folks that bring you MXUnit. More info on MightyMock can currently be found at http://sites.google.com/site/mightymock/.