You should spend all the time on creating mock objects that you have to … and no more. Here are the three principles that will help you decide how much you actually need to do.
Creating mock objects (and their siblings, like stubs and fakes) isn’t free so, while you should spend all the time on creating them that you need to, you shouldn’t spend any more time than you absolutely have to. Here are the three principles what will help you decide how much you actually need to do.
Testing is hard. So is planning software projects, but we’ve adopted agile processes to simplify that—primarily by reducing the planning window to four to six weeks, which appears to be as much as mere mortals can handle reliably. The unit test -> integration test -> end-to-end test hierarchy performs the same sort of simplification function: A test at any level in the hierarchy only has to handle the problems for its level and can ignore the rest. Similarly, stress testing, load testing and other test phases simplify what you need to do in, for example, unit testing by offloading stress and load issues.
However, while mock objects can simplify your testing in up to five ways, that doesn’t mean you always need to take advantage of them.
Of course, before you can use mock objects, you need an application that supports mocking: a loosely coupled architecture that lets you insert a mock object to replace a real object/dependency. Some form of dependency injection facilitates this, as do most design patterns. The major exception to this rule is Microsoft’s Fakes, where shims let you insert objects into compiled code to replace the original objects—however, Fakes is only available with Visual Studio Enterprise, which is expensive (many effective mocking tools are free, including Moq, Rhino Mocks, and Progress’ own Telerik JustMock Lite).
Tying your code’s object references to interfaces rather than classes also makes it easier to integrate mock objects into your testing (though, as I’ve discussed elsewhere, you can use mock objects even if you’re not leveraging interfaces).
You also need to be aware of what mock objects can do for you. Building on Gerard Meszaros’ discussion in xUnit Test Patterns, “test doubles” simplify testing by acting in up to five roles (I’m using “roles” and “test doubles” to avoid arguments about which of the resulting objects should properly called “mock objects”). Here are the five roles that I use test doubles for:
All of these roles simplify testing: Isolating the CUT ensures that you know where the problem is when a test fails, reports make it easier to check for specific kinds of failures, a minimal implementation reduces complexity associated with interacting with the “real” object, and so on.
But simplifying code by creating test doubles doesn’t necessarily mean eliminating code, which leads to the first principle of using any test double.
Before you start creating a test double, remember what your job is: to deliver working, reliable, production-ready code. Whatever strategy you adopt for testing, your job is not to create/manage/maintain test doubles.
Every line of code you add to a test double increases the time you’re not spending on production code—and that includes the maintenance costs associated with any set of code. On top of that, test doubles have their own, unique maintenance burden. For example, if the object that a test double replaces is changed, then all of its test doubles must also be updated (for a more in-depth discussion of the costs of maintaining test doubles, see Steve Bement’s post).
Yes, the reverse is also true: You should create test doubles everywhere it reduces your testing maintenance load. Every once in a while I meet someone who says that they don’t create objects to mock their database results because “we manage our test database so that it supports our tests.” I suspect that they’re spending far more time managing their test database than creating the relevant test doubles would require.
The key point, though, is that if you’re spending a significant amount of time keeping mock objects up to date so that your tests won’t fail—i.e. if your code is fine but your mock objects are frequently wrong—then you’re violating the first principle.
The second principle moves from your job to your tests: Remember the purpose of the test you’re building the test double for.
For unit tests, your goal is to prove that your CUT is “doing the right thing” when a particular set of inputs is provided to the CUT. For example, in a unit test, you only need to create a mock object to isolate the CUT when interaction with that “other object” could cause your test to fail or would result in unfortunate side effects (bombarding your customers with spurious emails, for example).
Taking a black-box approach toward your testing further simplifies testing by reducing the criteria around “doing the right thing” for any test: In a unit test, if you get the right result, then your CUT has passed. If you’re testing for “how well” the CUT did its job, then you’re probably getting into load or stress testing.
You should only create a test double if it both simplifies your testing and helps prove your CUT is passing the current level of testing.
And the first two principles lead to the third and final principle: If you do create a test double, don’t write any more code for it than you need to meet the needs of your test. As a corollary: When you do create a test double, favor a declarative approach that lets the framework handle creating the procedural details for you.
Tools can tempt you here. Telerik JustMock makes it easy to create test doubles that support any of the five roles (and does much of it declaratively). For call context-type tests, for example, you can check the sequence of the calls by adding the InOrder method to a test call. If you want to check how often a test double is called, you can just add one of the options in the Occurs enumeration to the call.
However, no matter how easy/obvious/declarative the process is, adding more test-double code increases the time to create your test double and the amount of maintenance that your test double will require. In addition, if you add more tests, you’ll probably feel obliged to test and review the results of those tests. And, of course, more tests mean more opportunities for failure … which is fine, provided you’re in the appropriate test. Inappropriate testing (no matter how well intentioned) takes you away from your job (see the first principle). Write what you need to pass your test. Then stop.
To sum up: When it comes to creating test doubles/mock objects, pick a good tool (it will make your life easier). Then use your mocking tool as much as you have to in order to meet the needs of your test (which may be “not at all” for some tests). Then run your tests and go back to writing your production code.
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter also writes courses and teaches for Learning Tree International.
Subscribe to be the first to get our expert-written articles and tutorials for developers!