-
Notifications
You must be signed in to change notification settings - Fork 0
/
14.4.txt
119 lines (83 loc) · 6.64 KB
/
14.4.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
113
114
115
116
117
118
119
14.4 Test-Specific Extensions
As the name suggests, a test-specific extension is an extension of an object that is specific to a particular test, or example in our case. We call them test-specific extensions because it is very similar to the test-specific subclass pattern described by Meszaros, in which a subclass of a real class is used to extend instances to support test-double-like behavior.
Thanks to Ruby’s metaprogramming model, we can get the same result by extending existing objects. And because the resulting object is partially the original object and partially a test double, we commonly refer to this technique as partial mocking and stubbing.
Partial Stubbing
Consider a case in Ruby on Rails where we want to disconnect the system we are working on from the database. We can use real objects but stub the find( ) and save( ) methods that we expect to be invoked.
Given, When, Then with Test Spies
Libraries like RR and the ironically named not-a-mock use the Test Spy pattern to provide a means of expressing message expectations in the past tense, thereby maintaining the flow of expectations at the end of an example.? As of this writing, RSpec::Mocks does not support test spies, but, luckily, both not-a-mock and RR plug right into RSpec and can be used instead of RSpec::Mocks if spies are what you’re after.
Here’s what our statement, customer, logger example might look like
using not-a-mock: †
describe Statement do
it "logs a message when on generate()" do
customer = stub('customer')
customer.stub(:name)
logger = mock('logger')
logger.stub(:log)
statement = Statement.new(customer, logger)
statement.generate
logger.should have_received(:log)
end
end
And here it is with RR:‡
describe Statement do
it "logs a message when on generate()" do
customer = Object.new
stub(customer).name
logger = Object.new stub(logger).log
statement = Statement.new(customer, logger)
statement.generate
logger.should have_received.log end
end
*. http://xunitpatterns.com/Test%20Spy.html
†. https://github.com/notahat/not_a_mock
‡. https://github.com/btakita/rr
Here’s an example:
describe WidgetsController do
describe "PUT update with valid attributes"
it "redirects to the list of widgets"
widget = Widget.new()
Widget.stub(:find).and_return(widget)
widget.stub(:update_attributes).and_return(true)
put :update, :id => 37
response.should redirect_to(widgets_path)
end
end
end
There are a few things going on in this example:
• We stub the class-level find() method to return a known value, in this case, the Widget object created on the previous line.
• We stub the update_attributes() method of the widget object, programming it to return true.
• We invoke the put( ) method from the Rails functional testing API.3
• We set an expectation that the response object should redirect to the list of widgets.
This example specifies exactly what the description suggests: Widgets-Controller PUT update with valid attributes redirects to the list of widgets. That the attributes are valid is a given in this example, and we don’t really need to know what constitutes valid attributes in order to specify the controller’s behavior in response to them. We just program the Widget to pretend it has valid attributes.
This means that changes to the Widget’s validation rules will not have any impact on this example. As long as the controller’s responsibility does not change, this example won’t need to change, nor will the con- troller itself.
There is also no dependency on the database in this example—well, no explicit dependency. Rails will try to load up the schema for the widgets table the first time it loads widget.rb, but that is the only database interaction. There are no additional database interactions as a result of this example. If we use a Rails plug-in like NullDB,4 we can completely disconnect from the database, and this example will still run.
3. As you’ll learn about in Chapter 24, Rails Controllers, on page 345, the rspec-rails library provides rspec-flavored wrappers around Rails’ built-in testing facilities.
Partial Mocking
In the WidgetsController example, it is possible to get it to pass without ever actually finding a widget or updating its attributes. As long as the controller method redirects to the widgets_path, that example passes. For this reason and for the purposes of documentation, we may want separate examples that specify these details. For these examples, we can set message expectations on the Widget class and instance instead of method stubs. This is called partial mocking. Here’s what this might look like:
describe WidgetsController do
describe "PUT update with valid attributes"
it "finds the widget"
widget = Widget.new()
widget.stub(:update_attributes).and_return(true)
Widget.should_receive(:find).with("37").and_return(widget)
put :update, :id => 37
end
it "updates the widget's attributes" do
widget = Widget.new()
Widget.stub(:find).and_return(widget)
widget.should_receive(:update_attributes).and_return(true)
put :update, :id => 37
end
end
end
Note how we mix method stubs and message expectations in these examples. The first example specifies that the WidgetsController finds the widget, so we set an expectation that the Widget class should receive the find( ) method. We need to program the widget to return true for update_attributes( ), but we’re not specifying that it is called in this exam- ple, so we just use a method stub.
Message expectations on the real model objects allow us to specify how the controller interacts with them, rather than a specific outcome.
4. http://avdi.org/projects/nulldb/
These two examples, combined with the redirect example in which we used only method stubs on the model objects, produce the following output:
WidgetsController
PUT update with valid attributes
finds the widget
updates the widget's attributes
redirects to the list of widgets
As you can see just from the output, these techniques help us spec- ify what the WidgetsController does. And by using different techniques in different examples, we are able to keep each example focused on a specific granular facet of the behavior.
Partial stubbing/mocking isn’t risk free. We must take care not to replace too much of the real objects with stub/mock methods. This is especially important for the subject of the example, because we end up not working with the object we thought we were. So, keep partial mocking to an absolute minimum.