Strategies for Using Test Doubles

Test doubles are like any tool; the bigger challenge is not in learning how to use them but in knowing when to use them. This section describes a few schools of thought and provides some recommendations for appropriate use of test doubles.

Exploring Design

Suppose AddressExtractor does not exist. When you test-drive summaryDescription, you’ll of course recognize the need for logic that takes a JSON response and ultimately returns a formatted string. You could code the entire implementation for that in the PlaceDescriptionService. It’s not much code (a little more than a dozen lines, based on the code in AddressExtractor as it exists).

Some programmers always wear the designer hat, seeking designs that exhibit the potential for reuse, increased flexibility, and improved ease of understanding the code. To adhere to the SRP, they might break the required logic into two needs: parsing the JSON response and formatting the output.

TDD allows you...no, it requires you to make conscious design choices at all times. You have the option of implementing the summaryDescription logic in an infinite number of ways. TDD helps you explore that design. Often that’s done by coding something that works and then refactoring to an appropriate solution.

Or, you could first write a test that describes how summaryDescription should interact with a collaborator. The job of this collaborator is to take a JSON response and return a corresponding address data structure. For the time being, you ignore the details of how to implement the collaborator. You focus instead on test-driving the implementation of summaryDescription using mocks, just as we did for interactions with the Http object.

When test-driving in this manner, you introduce mocks to supply otherwise-missing collaborator behavior. You design interfaces for the collaborators based on the interests and needs of the client.

At some point, you or someone else will implement the collaborator. You have a choice: remove the mocks so that the code under test uses the production collaborator or keep the mocks in place.

The issue may already be decided for you. If the collaborator introduces troublesome dependencies, you’ll need to retain the mock. If it does not, removing the mock means removing a bit of extra complexity in your tests. However, you might choose to retain the mocks, particularly if interactions with the collaborator are an important aspect of design that you should describe.

The best guideline probably takes into account the effort required to maintain and understand the tests. It can be simpler without mocks in place, but that’s not always the case. It can require a lot of setup code to initialize some collaborators, which can increase your effort to maintain the tests.

Too Much Mocking?

For seven months in 2003, I worked as a programmer on a large Java development team doing XP. On arrival, I was enthusiastic about seeing a large number of unit tests but not quite so enthusiastic about the test quality or production code quality. None of it was terrible, but I noted too much duplication, methods and tests longer than necessary, and heavy use of mocks. The system worked, though, exhibiting very few defects.

Months later, attempts to scale the system from eleven to more than fifty users resulted in performance issues. All signs pointed to suboptimal use of the middleware framework as the culprit. A team of high-dollar middleware framework experts worked closely with our team to rework the code.

Many of the mocks for controller-level code verified sequences of events by expecting that methods were called in a certain order. (Worse, the tool they used required method expectations to be represented as strings. Renaming a method meant you had to remember to update the string literals used by the mocks.) But these methods were not as abstract as they might have been. When the optimizing team started reworking the code to improve performance, they often changed the underlying design of a given message flow through the system. That meant moving methods around, renaming methods, compressing two methods into one, deleting others, and so on. Every time they made such a change...uh oh! Tests broke, sometimes more than a dozen at once.

Since the tests were tightly coupled to the target implementation, transforming the code went very slowly. Yelling by the VPs and other hostilities commenced. All programmers and TDD itself were called into question.

Having tests highly dependent on implementation specifics—which methods are called in which order—created significant problems. But in hindsight, the far larger problem was our inadequate system design. We might have had an easier time had we factored away duplication across tests. Another significant problem was not having appropriate performance/scaling tests.

Schools of Mock

Practitioners who view TDD primarily as a design exploration tool fall into what’s sometimes called the London school. Founders of this school include Tim MacKinnon, Steve Freeman, and Philip Craig, authors of the original paper on mocks, Endo-Testing: Unit Testing with Mock Objects [MFC01]. The highly regarded book Growing Object-Oriented Software, Guided by Tests [FP09], by Freeman and Nat Pryce, focuses on using TDD to grow out a system in this manner.

Because of its emphasis on object interactions, the London school approach promotes the notion of Tell-Don’t-Ask. In an object-oriented system, you (a client object) want to tell an object to do something by sending it a message and letting it go do its work. You don’t want to ask an object for information and then do work that could be the responsibility of that object. Tell-Don’t-Ask promotes a more decoupled design.

The classic school (sometimes called the Cleveland school) emphasizes verification of behavior by inspecting state. Kent Beck’s book Test Driven Development: By Example [Bec02] focuses almost entirely on this approach to TDD. Folks in this camp avoid introducing mocks until a dependency concern forces the issue.

Introducing a mock creates a dependency of the tests on the implementation details of the target. If you employ a tool, you also create a dependency on that tool. Both dependencies can make your designs more rigid and your tests more fragile if you’re not careful. The best defense against challenges created by dependencies is good design: isolate and minimize them.

Using mocks also generates additional complexity that you’ll pay for (as I just did, wasting several minutes with my mock declaration mistake).

As a professional, you owe it to yourself to learn about both approaches. While you might choose to follow one school or another, it’s possible to incorporate elements of both London and classic schools into your TDD practice.

Using Test Doubles Wisely

If you want a fully test-driven system with fast tests, the predominance of systems will require you to use test doubles. When you use test doubles, consider the following recommendations:

Reconsider the design.

Does your compulsion to mock exist in order to simplify creation of dependent objects? Revisit your dependency structure. Are you mocking the same thing in multiple places? Restructure the design to eliminate this duplication.

Recognize the concession to unit testing coverage.

A test double represents a hole of sorts in your system’s coverage. The lines of logic that your test double supplants is code that your unit tests will not execute. You must ensure that other tests cover that logic.

Refactor your tests.

Don’t let your increased dependency on a third-party tool create a problem. A haphazard approach can quickly generate rampant mocking, resulting in lots of duplication and otherwise difficult tests. Refactor your tests as much as you refactor your production code! Encapsulate expectation declarations in common helper methods to improve abstraction, reduce the extent of the dependency, and minimize duplicate code. When you later want to upgrade to a newer, better tool than Google Mock, you won’t be faced with quite as devastating a change.

Question overly complex use of test doubles.

If you’re struggling with mocks, it could be because either you’re trying to test too much or your design is deficient. Having multiple levels of mocks is usually a recipe for headaches. Using fakes, as discussed in Miscellaneous Test Double Topics, usually leads to more struggle. If stuck, simplify what you’re trying to do by breaking things into smaller tests. Look also at the potential of splitting apart the code you are testing.

Choose expressiveness over power.

Choose your mock tool because it helps you create highly abstract tests that document behaviors and design, not because it has cool features and can do clever, esoteric things. Use those clever, esoteric features only when you must.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.62.122