-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path13.2.txt
270 lines (169 loc) · 14.2 KB
/
13.2.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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
13.2 Built-in Matchers
RSpec ships with several built-in matchers with obvious names that you can use in your examples. In addition to equal(expected), others include the following:
include(item)
respond_to(message)
raise_error(type)
By themselves, they seem a bit odd, but in context they make a bit more sense:
prime_numbers.should_not include(8)
list.should respond_to(:length)
lambda { Object.new.explode! }.should raise_error(NameError)
We will cover each of RSpec’s built-in matchers, starting with those related to equality.
Equality: Object Equivalence and Object Identity
Although we’re focused on behavior, many of the expectations we want to set are about the state of the environment after some event occurs. The two most common ways of dealing with post-event state are to specify that an object should have values that match our expectations (object equivalence) and to specify that an object is the very same object we are expecting (object identity).
Most xUnit frameworks support something like assert_equal to mean that two objects are equivalent and assert_same to mean that two objects are really the same object (object identity). This comes from languages like Java, in which there are really only two constructs that deal with equality: the == operator, which, in Java, means the two references point to the same object in memory, and the equals method, which defaults to the same meaning as == but is normally overridden to mean equivalence.
Note that you have to do a mental mapping with assertEqual and assertSame. In Java, assertEqual means equal, and assertSame means ==. This is OK in languages with only two equality constructs, but Ruby is a bit more complex than that. Ruby has four constructs that deal with equality.
a == b
a === b
a.eql?(b)
a.equal?(b)
Each of these has different semantics, sometimes differing further in different contexts, and can be quite confusing.1 So, rather than forc- ing you to make a mental mapping from expectations to the methods they represent, RSpec lets you express the exact method you mean to express.
a.should == b
a.should === b
a.should eql(b)
a.should equal(b)
The most common of these is should ==, because the majority of the time we’re concerned with value equality, not object identity. Here are some examples:
(3 * 5).should == 15
person = Person.new(:given_name => "Yukihiro", :family_name => "Matsumoto")
person.full_name.should == "Yukihiro Matsumoto"
person.nickname.should == "Matz"
In these examples, we’re only interested in the correct values. Some- times, however, we’ll want to specify that an object is the exact object that we’re expecting.
person = Person.create!(:name => "David")
Person.find_by_name("David").should equal(person)
This puts a tighter constraint on the value returned by find_by_name( ), requiring that it must be the exact same object as the one returned by create!( ). Although this may be appropriate when expecting some sort of caching behavior, the tighter the constraint, the more brittle the expectation. If caching is not a real requirement in this example, then saying Person.find_by_name("David").should == person is good enough and means that this example is less likely to fail later when things get refactored.
1. See http://www.ruby- doc.org/core/classes/Object.html#M001057 for the official documentation about equality in Ruby.
Do Not Use !=
Although RSpec supports the following:
actual.should == expected
it does not support this:
# unsupported actual.should != expected
For the negative, you should use this:
actual.should_not == expected
The reason for this is that == is a method in Ruby, just like to_s( ), push( ), or any other method named with alphanumeric characters. The result is that the following:
actual.should == expected
is interpreted as this:
actual.should.==(expected)
This is not true for !=. Ruby interprets this:
actual.should != expected
as follows:
!(actual.should.==(expected))
This means that the object returned by should( ) receives == whether the example uses == or !=. And that means that short of doing a text analysis of each example, which would slow things down considerably, RSpec cannot know that the example really means != when it receives ==. And because RSpec doesn’t know, it won’t tell you, which means you’ll be getting false responses. So, stay away from != in examples.
Floating-Point Calculations
Floating-point math can be a pain in the neck when it comes to setting expectations about the results of a calculation. And there’s little more frustrating than seeing “expected 5.25, got 5.251” in a failure message, especially when you’re only looking for two decimal places of precision.
To solve this problem, RSpec offers a be_close matcher that accepts an expected value and an acceptable delta. So, if you’re looking for precision of two decimal places, you can say the following:
result.should be_close(5.25, 0.005)
This will pass as long as the given value is within .005 of 5.25.
Multiline Text
Imagine developing an object that generates a statement. You could have one big example that compares the entire generated statement to an expected statement. Something like this:
expected = File.open('expected_statement.txt','r') do |f|
f.read
end
account.statement.should == expected
This approach of reading in a file that contains text that has been reviewed and approved and then comparing generated results to that text is known as the “Golden Master” technique and is described in detail in J.B. Rainsberger’s JUnit Recipes [Rai04].
This serves very well as a high-level code example, but when we want more granular examples, this can sometimes feel a bit like brute force, and it can make it harder to isolate a problem when the wheels fall off.
Also, there are times that we don’t really care about the entire string, just a subset of it. Sometimes we only care that it is formatted a specific way but don’t care about the details. Sometimes we care about a few details but not the format.
In any of these cases, we can expect a matching regular expression using either of the following patterns:
result.should match(/this expression/)
result.should =~ /this expression/
In the statement example, we might do something like this:
statement.should =~ /Total Due: \$37\.42/m
One benefit of this approach is that each example is, by itself, less brittle and less prone to fail due to unrelated changes. RSpec’s own code examples are filled with expectations like this related to failure messages, where we want to specify certain things are in place but don’t want the expectations to fail because of inconsequential formatting changes.
Ch, ch, ch, ch, changes
Ruby on Rails extends Test::Unit with some Rails-specific assertions. One such assertion is assert_difference( ), which is most commonly used to express that some event adds a record to a database table, like this:
assert_difference 'User.admins.count', 1 do
User.create!(:role => "admin")
end
This asserts that the value of User.admins.count will increase by 1 when you execute the block. In an effort to maintain parity with the Rails assertions, RSpec offers this alternative:
expect {
User.create!(:role => "admin")
}.to change{ User.admins.count }
You can also make that more explicit if you want by chaining calls to by( ), to( ) and from( ).
expect {
User.create!(:role => "admin")
}.to change{ User.admins.count }.by(1)
expect {
User.create!(:role => "admin")
}.to change{ User.admins.count }.to(1)
expect {
User.create!(:role => "admin")
}.to change{ User.admins.count }.from(0).to(1)
This does not work only with Rails. You can use it for any situation in which you want to express a side effect of some event. Let’s say you want to specify that a real estate agent gets a $7,500 commission on a $250,000 sale:
expect {
seller.accept Offer.new(250_000)
}.to change{agent.commission}.by(7_500)
Now you could express the change by explicitly stating the expected starting and ending values, like this:
agent.commission.should == 0 seller.accept Offer.new(250_000)
agent.commission.should == 7_500
This is pretty straightforward and might even be easier to understand at first glance. Using expect to change, however, does a nice job of identifying what the event is and what the expected outcome is. It also functions as a wrapper for more than one expectation if you use the from( ) and to( ) methods, as in the previous examples.
So, which approach should you choose? It really comes down to a matter of personal taste and style. If you’re working solo, it’s up to you. If you’re working on a team, have a group discussion about the relative merits of each approach.
Expecting Errors
When first learning Ruby, you might get a sense that the language is reading your mind. Say you need a method to iterate through the keys of a Ruby hash so you type hash.each_pair {|k,v| puts k} just to see if it works, and, of course, it does! And this makes you happy!
Ruby is filled with examples of great, intuitive APIs like this, and it seems that developers who write their own code in Ruby strive for the same level of obvious, inspired by the beauty of the language. We all want to provide that same feeling of happiness to developers that they get just from using the Ruby language directly.
Well, if we care about making developers happy, we should also care about providing meaningful feedback when the wheels fall off. We want to provide error classes and messages that provide context that will make it easier to understand what went wrong.
Here’s a great example from the Ruby library:
$ irb
irb(main):001:0> 1/0
ZeroDivisionError: divided by 0
from (irb):1:in `/'
from (irb):1
The fact that the error is named ZeroDivisionError probably tells you everything you need to know to understand what went wrong. The message “divided by 0” reinforces that. RSpec supports the development of informative error classes and messages with the raise_error( ) matcher.
If a checking account has no overdraft support, then it should let us know:
account = Account.new 50, :dollars
expect {
account.withdraw 75, :dollars
}.to raise_error(
InsufficientFundsError,
/attempted to withdraw 75 dollars from an account with 50 dollars/
)
The raise_error( ) matcher will accept zero, one, or two arguments. If you want to keep things generic, you can pass zero arguments, and the example will pass as long as any subclass of Exception is raised.
expect { do_something_risky }.to raise_error
The first argument can be any of a String message, a Regexp that should match an actual message, or the class of the expected error.
expect {
account.withdraw 75, :dollars
}.to raise_error(
"attempted to withdraw 75 dollars from an account with 50 dollars"
)
expect {
account.withdraw 75, :dollars
}.to raise_error(/attempted to withdraw 75 dollars/)
expect {
account.withdraw 75, :dollars
}.to raise_error(InsufficientFundsError)
When the first argument is an error class, it can be followed by a second argument that is either a String message or a Regexp that should match an actual message.
expect {
account.withdraw 75, :dollars
}.to raise_error(
InsufficientFundsError,
"attempted to withdraw 75 dollars from an account with 50 dollars"
)
expect {
account.withdraw 75, :dollars
}.to raise_error(
InsufficientFundsError,
/attempted to withdraw 75 dollars/
)
Which of these formats you choose depends on how specific you want to get about the type and the message. Sometimes you’ll find it useful to have several code examples that get into details about messages, while others may just specify the type.
If you look through RSpec’s own code examples, you’ll see many that look like this:
expect {
@mock.rspec_verify
}.to raise_error(MockExpectationError)
Since there are plenty of other examples that specify details about the error messages raised by message expectation failures, this example only cares that a MockExpectationError is raised.
Expecting a Throw
Like raise( ) and rescue( ), Ruby’s throw( ) and catch( ) allow us to stop execution within a given scope based on some condition. The main difference is that we use throw/catch to express expected circumstances as opposed to exceptional circumstances.
Let’s say we’re writing an app to manage registrations for courses at a school, and we want to handle the situation in which two students both try to register for the last available seat at the same time. Both were looking at screens that say the course is still open, but one of them is going to get the last seat, and the other is going to be shut out.
We could handle that by raising a CourseFullException, but a full course is not really exceptional. It’s just a different state. We could ask the Course if it has availability, but unless that query blocks the database, that state could change after the question is asked and before the request to grab the seat is made.
This is a great case for try/catch, and here’s how we can spec it:
Download expectations/course_full.rb
course = Course.new(:seats => 20)
20.times { course.register Student.new }
lambda {
course.register Student.new
}.should throw_symbol(:course_full)
Like the raise_error( ) matcher, the throw_symbol( ) matcher will accept zero, one, or two arguments. If you want to keep things generic, you can pass zero arguments, and the example will pass as long as any- thing is thrown.
The first (optional) argument to throw_symbol( ) must be a Symbol, as shown in the previous example.
The second argument, also optional, can be anything, and the matcher will pass only if both the symbol and the thrown object are caught. In our current example, that would look like this:
Download expectations/course_full.rb
course = Course.new(:seats => 20)
20.times { course.register Student.new }
lambda {
course.register Student.new
}.should throw_symbol(:course_full, 20)