Saturday, January 24, 2004

An Introduction to Mock Objects as a Testing Strategy

Prologue

Mock Objects are actors who play roles in your test scenarios. They reside with your test code, and provide a clear and simple way to unit-test certain hard-to-test conditions. Mixed with a test-driven approach to software design, they can also point the way to surprisingly flexible code. Unfortunately, Mock Objects used incorrectly can add clutter and confusion. But each test really boils down to three players: The test code, the code being tested, and the Mock Object. Picture those three players as separate but important entities, and you’ll begin to see your next test as a short, simple act in an ongoing play.

Act I

Suppose we want to test an object called “Student.” We aren’t going to test Student to see what it knows: We want to know if Student knows how to do some required research. In other words, we’re going to write a simple test to determine if Student collaborates correctly.

We want to ask Student the question secondPresidentOfTheUnitedStates(). We're not really concerned whether or not Student knows the answer, but we want to verify that Student knows when to seek help from another object, Historian.

The real Historian is, of course, a magical source of knowledge. One of the amazing things that the true Historian can do is answer listOfUSPresidents(). But we have decided that Historian is too expensive (and slow) to use whenever we want to run tests, especially since we're not trying to test Historian (who has its own extensive suite of unit-tests, to be sure). So we build a MockHistorian.

MockHistorian starts out as a mere shell of an object. We tell MockHistorian exactly what to reply when asked listOfUSPresidents(). For our test, we need only two Presidents. So, we tell MockHistorian to respond with a List of two Presidents, "Washington" and "Jefferson".

Then, in our unit-test, we introduce Student to MockHistorian, explaining that MockHistorian is the Historian to use. Yes, we lie to the object under scrutiny if necessary, but what we’re actually doing is setting up the Student object’s “state”. After all, the Student’s state is, at any given time, the whole of all Student’s member variables. This includes a reference to a Historian object. That is, in a nutshell, what Mock Objects do: They represent fake state within the tested object.

After setting the stage for our test, we ask Student the secondPresidentOfTheUnitedStates() question. Student will need to reply with President "Jefferson", otherwise Student fails the test.

Note that the test does not depend on whether or not Jefferson was the 2nd president of the United States. (He wasn’t. "Dammit, Jim, I'm a programmer, not a historian...") This is a unit-test of Student, not a test of Historian (or programmer). Note, also, that we’re not actually testing the Student's knowledge of history, but rather the Student's ability to collaborate with Historian. "Is the Student capable of querying the Historian?" That's all we're going to test in this one teeny-tiny unit-test. It is a boring, but essential behavior. When Student is capable of doing that, we'll come up with some harder questions.

Act II

What if, after getting the first test to pass, we ask Student numberOfLivingUsPresidents()? Yes, we're going to have to improve MockHistorian as well as Student. Both objects will evolve. MockHistorian should always remain incredibly simple, but must also continue to implement the same public interface as Historian. MockHistorian is an actor in the service of the unit-tests, not a throwaway object. Eventually, MockHistorian may become very good at answering all questions that Historian answers, even if all the answers are prescribed.

Boring, right? Quite a dull part for the acting talents of the MockHistorian. You tell MockHistorian what to say, you ask Student for some information and assert that Student answers with the expected results. Is the test done? Perhaps not. MockHistorian can also act as a spy. Once the transaction between Student and Historian is complete, you can query MockHistorian for certain details. Did Student call a particular Historian method once, or did Student get confused and ask for the same information numerous times?

Your MockHistorian can also act out a failure condition. What should Student do if Historian is unable to check its resources? Rather than setting up a test with a real Historian, (and trying to remember to pull the Ethernet cable out of the test machine at the right moment), you can tell MockHistorian to fake it. Have MockHistorian throw a LibraryOfCongressIsClosedTodayException, and check Student’s reaction.

Act III

When working with mocks, you may get the feeling that you're not testing anything. Good! You're on the right track. You’re usually unit-testing a single scenario for a single method on a single object. If you try to cover too much territory with one test, you may not be able to tell why it fails. Eventually it will become easier to write a dozen very small tests rather than one huge test for the same set of related behavior. These tests document various bits of required behavior, and will help pinpoint problems as they arise.

Keep each individual test as simple as possible, while still exercising a useful Student behavior. Each test shouldn't test much, at all. Try to determine what you really need to ask the tested object in order to drive the simplest implementation. Get the tested object to collaborate when possible, rather than doing all the work. If you discover that Historian doesn't answer something that Student needs to know, ask yourself which object should have what pieces of the puzzle. Write tests for both Student and Historian, if necessary. Such tests keep us from giving one object too much responsibility.

A common pitfall is to write a test for Student using MockSTUDENT. Testing Student by testing MockStudent is like asking a TV Doctor to perform brain surgery: The actors have fooled even you, the director. If you have the urge to write a test for a Mock Object, you are picking up cues that your object has taken on too much responsibility. Perhaps it also tends to rely on numerous private methods for various conditional behavior. There may be two objects, two separate groups of data and behavior, disguised as one. Find the new object, and let the original collaborate with it.

Epilogue

As you can tell from this simple example, Mock Objects lead you toward highly collaborative solutions (and away from Singletons and bloated objects). These design choices are not necessarily better or worse than other designs, in the classical sense, but they tend to be more testable. And tested, testable code is certainly very high quality code.

1 comment:

Rob Myers said...

Update:

Mock object frameworks have matured considerably. Check out

http://www.mockobjects.com/

for the right framework for your team.