Unit Testing Done Right with Mock Objects

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;

  1. 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.
  2. 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!

Bookmark and Share

12 Comments

Posted By: Bob Silverberg at 03.13.2010 2:04 PM
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/.

Posted By: Brian Carr at 03.13.2010 3:31 PM
Thanks Bob, I'm glad you liked it. I could be wrong but it feels like mocking is unfortunately under-utilized in general within the community. I'm really liking the simple-example provided by the MightyMock documentation - I plan to delve further into it this weekend for sure - thanks for the link!

Posted By: Bob Silverberg at 03.13.2010 10:01 PM
Well, I think that unit testing is under-utilized as well, and as mocking seems to be sort of a "next step" after tackling unit testing it's totally understandable (that it would be under-utilized as well). I'm personally hoping that adding the built-in mocking support to MXUnit will encourage more people to give it a try. I suppose more blog posts on the subject wouldn't hurt either. ;-)

I'll add those to my ever increasing list of things to do.

Posted By: Brian Carr at 03.14.2010 8:40 PM
Agreed - in fact I should also mention to readers that Micky did an excellent write-up on the same subject several months ago, the good word must be spread; http://mickydionisio.blogspot.com/search/label/mocking

Posted By: Luis Majano at 03.16.2010 1:02 PM
nice post Brian! Mocking really has changed the way I do testing. Not only that, it brings a certain "WOWness" to it that makes my testing not boring anymore. Glad you like MockBox, there are some interesting updates coming in the next release and some more docs. I will be sure to point to this blog entry as a resource also.

Posted By: Brian Carr at 03.16.2010 4:51 PM
Thanks Luis. I look forward to seeing docs and enhancements for the next release - and thanks for the mention on the MockBox blog!

Posted By: bill shelton at 03.17.2010 4:34 AM
Nice piece, Brian--well thought out and articulate. Thanks.

The one aspect about stubbing/mocking that I take with me, is the concept of "asking" vs. "telling", which speaks to state verification vs. behavior verification. In other words, when I call a method on an object I'm either "asking" it to return some data or "telling" it to go off and do some task (return void). Mocking and the ability to verify void calls becomes much more important with the latter. How do you test a method that returns void? I think a subtle temptation (or smell?) creeps in, and that is, to return a simple value (such as a return code) for a method in order to perform verification, either by the test or the client. With a mocking approach, you verify the methods inside your SUT were called with specific parameters and in the correct order, at test time vs. runtime. It's kind of like having a clean window into a black box.

Funny story: I started MightyMock last year and at cf.Objective() Luis was like, "Hey, I did something similar-MockBox!". Neither of us knew what the other was doing, and we had many good talks about mocking there and afterwards. Unlike me, Luis is insanely productive, and MockBox is very nice. Also, as Bob mentioned, MXUnit will have MightyMock built in (simply) and also the ability to wire in other mocking frameworks like MockBox, too. So, there will be a couple of mocking flavors available for CF.

Rock on, bill

Posted By: Brian Carr at 03.17.2010 11:10 AM
Bill - I completely agree, augmenting the API of the SUT for no other reason than to make it more test-friendly is a big no-no IMO. Your "void method" example is an excellent point - another scenario further underscoring the value of mocking.

I'm with Bob, more blog posts on the subject definitely would not hurt :-)

Posted By: Aaron Greenlee at 03.18.2010 8:08 AM
Your first post of the series (hint) is well written and will help many in the community!

Posted By: Brian Carr at 03.18.2010 6:07 PM
Thanks Aaron - hint taken :-)

Posted By: Dustin Chesterman at 06.09.2010 5:42 PM
Always good to have another description of this lightly documented topic. I am trying to wrap my head around another part of unit testing currently and was wondering if mocking would be helpful.

Basically, I want to test the dao's db queries and make sure they are executable with the properties of an entity... make sure my entity definitions and my db queries are in sync.

But I can't practically rollback a transaction insertion of a temporary record due to huge dependencies in the db schema. Is there a way to test this without loading 50 different db dependency records first every time?

Posted By: Aaron Greenlee at 06.09.2010 7:16 PM
I like to do something like this and have my unit test execute 'executeSave' and my application execute 'save'

<cffunction name="save" access="public" returntype="boolean" hint="Save handler."> <cfset var result = false /> <cftransaction action='begin'> <cftry> <cfset result = executeSave() /> <cftransaction action='commit' /> <cfcatch><cftransaction action='rollback' /></cfcatch> </cftry> </cftransaction> <cfreturn result /> </cffunction>

<cffunction name="executeSave" access="public" returntype="boolean" hint="Save execution."> <cfquery> INSERT INTO USER (`Email`, `Password`, `DateCreated`, `DateModified`, `DateLastVisited`) VALUES ( 'aaron@aaron.com','asdasd',now(),now(),now() ); </cfquery> <cfreturn true /> </cffunction>

Post a Comment







Stay Connected

Recent Entries

Recent Comments