-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path14.7.txt
113 lines (64 loc) · 8.46 KB
/
14.7.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
14.7 When to Use Test Doubles and Test-Specific Extensions
Now that we know how to use test doubles and test-specific extensions, the next question is when to use them! There are a lot of opinions about this, and we’re not going to be able to cover every topic (this could easily fill an entire book), but let’s look at some guidelines that can help you navigate your way.
Isolation from Dependencies
Even the most highly decoupled code has some dependencies. Sometimes they are on objects that are cheap and easy to construct and have no complex state. These generally don’t present a problem, so there is no need to create stubs for them.
The problematic dependencies are the ones that are expensive to construct, involve external systems (network, servers, even the file system), have dependencies on other expensive objects, or function slowly. We want to isolate our examples from these dependencies because they complicate setup, slow down runtimes, and increase potential points of failure.
Consider the system depicted in Figure 14.1, on the next page, with dependencies on a database and a network connection. We can replace the dependencies with test doubles, as shown in Figure 14.2, on the following page, thereby removing the real dependencies from the process. Now we are free of any side effects arising from external systems.
Isolation from Nondeterminism
Depending on external systems can also be a source of nondeterminism. When we depend on components with nondeterministic characteristics, we may find that files get corrupted, disks fail, networks time out, and servers go down in the middle of running specs. Because these are things that we have no control over, they can lead to inconsistent and surprising results when we run our specs.
Database
Interface Database
Subject
Network
Interface
Internet
Figure 14.1: External dependencies
Code Subject Stub
Example
Database
Stub
Network
Figure 14.2: Stubbed dependencies
Subject
Figure 14.3: Dependency on a random generator
Doubles can disconnect our examples from real implementations of these dependencies, allowing us to specify things in a controlled environment. They help us focus on the behavior of one object at a time without fear that another might behave differently from run to run.
Nondeterminism can also be local. A random generator may well be local but is clearly a source of nondeterminism. We would want to replace the real random generator with stable sequences to specify different responses from our code. Each example can have a pseudorandom sequence tailored for the behavior being specified.
Consider a system that uses a die, like the one shown Figure 14.3. Because a die is a random generator, there is no way to use it to write a deterministic example. Any specifications would have to be statistical in nature, and that can get quite complicated. Statistical specs are useful when we’re specifying the random generators directly, but when we’re specifying their clients, all that extra noise takes focus away from the behavior of the object we should be focused on.
If we replace the die with something that generates a repeatable sequence, as shown in Figure 14.4, on the following page, then we can write examples that illustrate the system’s behavior based on that sequence. A stub is perfect for this, because each example can specify a different sequence.
Making Progress Without Implemented Dependencies
Sometimes we are specifying an object whose collaborators haven’t been implemented yet. Even if we’ve already designed their APIs, they might be on another team’s task list and just haven’t gotten to it yet.
Code Stub
Example Subject Die 3,20,7,14,1, ...
Figure 14.4: Dependency on a repeatable sequence
Rather than break focus on the object we’re specifying to implement that dependency, we can use a test double to make the example work. Not only does this keep us focused on the task at hand, but it also provides an opportunity to explore that dependency and possible alter - native APIs before it is committed to code.
Interface Discovery
When we’re implementing the behavior of one object, we often discover that it needs some service from another object that may not yet exist. Sometimes it’s an existing interface with existing implementations, but it’s missing the method that the object we’re specifying really wants to use. Other times, the interface doesn’t even exist at all yet. This process is known as interface discovery and is the cornerstone of mock objects.
In cases like these, we can introduce a mock object, which we can program to behave as the object we are currently specifying expects. This is a very powerful approach to writing object-oriented software, because it allows us to design new interfaces as they are needed, mak- ing decisions about them as late as possible, when we have the most information about how they will be used.
Focus on Role
In 2004, Steve Freeman, Nat Pryce, Tim Mackinnon, and Joe Walnes presented a paper entitled “Mock Roles, not Objects.”5 The basic premise is that we should think of roles rather than specific objects when we’re using mocks to discover interfaces.
5. http://mockobjects.com/files/mockrolesnotobjects.pdf
In the logging example in Section 14.3, Mixing Method Stubs and Message Expectations, on page 196, the logger could be called a logger, a messenger, a recorder, a reporter, and so on. What the object is doesn’t matter in that example. The only thing that matters is that it represents an object that will act out the role of a logger at runtime. Based on that example, in order to act like a logger, the object has to respond to the log( ) method.
Focusing on roles rather than objects frees us up to assign roles to different objects as they come into existence. Not only does this allow for very loose coupling between objects at runtime, but it provides loose coupling between concepts as well.
Focus on Interaction Rather Than State
Object-oriented systems are all about interfaces and interactions. An object’s internal state is an implementation detail and not part of its observable behavior. As such, it is more subject to change than the object’s interface. We can therefore keep specs more flexible and less brittle by avoiding reference to the internal state of an object.
Even if we already have a well-designed API up front, mocks still provide value because they focus on interactions between objects rather than side-effects on the internal state of any individual object.
This may seem to contradict the idea that we want to avoid implementation detail in code examples. Isn’t that what we’re doing when we specify what messages an object sends? In some cases, this is a perfectly valid observation. Consider this example with a method stub:
describe Statement do
it "uses the customer's name in the header (with a stub)" do
customer = stub("customer", :name => "Dave Astels")
statement = Statement.new(customer)
statement.header.should == "Statement for Dave Astels"
end
end
Now compare that with the same example using a message expectation instead:
describe Statement do
it "uses the customer's name in the header (with a mock)" do
customer = mock("customer")
customer.should_receive(:name).and_return("Dave Astels")
statement = Statement.new(customer)
statement.header.should == "Statement for Dave Astels"
end
end
Figure 14.5: Focus on state vs. interaction
In this case, there is not much value added by using a message expectation in the second example instead of the method stub in the first example. The code in the second example is more verbose and more tightly bound to the underlying implementation of the header( ) method. Fair enough. But consider the logger example earlier this chapter. That is a perfect case for a message expectation, because we’re specifying an interaction with a collaborator, not an outcome.
A nice way to visualize this is to compare the left and right diagrams in Figure 14.5. When we focus on state, we design objects. When we focus on interaction, we design behavior. There is a time and place for each approach, but when we choose the latter, mock objects make it much easier to achieve.