A Post Entitled RSpec best practices
A few days ago Philippe Creux posted his RSpec Best Practices and Tips. The post is well worth reading even if you prefer another test framework (you are testing, right?!) because there are several tips that are applicable to TDD more broadly than rspec alone. Of all the tips there are two that I cannot re-emphasize strongly enough: “Only One Expectation Per It Block” and “(Over)Use Describe and Context”.
Single Expectation Tests
The “one expectation” tip is more broadly expressed as “each test should make only one assertion.” Philippe’s example is a good one: rather than have one test that asserts that all expected attributes are present, have multiple tests that assert that individual attributes are present. The advantage of this approach lies in its granularity. The “one expectation” way allows to you to more quickly identify when multiple expectations have been violated. Why? Because a monolithic test will fail on the first failed expectation even though there may be multiple points of failure behind it.
The impetus for a monolithic test is typically the perceived advantage of collecting all the similar expectations into one place. Grouping the expectations does make it simpler to determine where to alter or extend that type of testing. Taking Philippe’s example, if you wanted to replace ‘age’ in the list of attributes with ‘dob’ (date of birth) and add ‘shirt_size’ then you would know right where to go to do that type of thing. For this reason I would make one additional recommendation that I’ve mentioned in the past: use the dynamic nature of ruby to your advantage by using code to create expectations. For example, rather than the recommended syntax
it { should respond_to :name }
it { should respond_to :age }
it { should respond_to :gender }
Try the following
[:name, :age, :gender].each do |expected_attribute|
it { should respond_to expected_attribute }
end
This code retains the advantages of both co-locating similar kinds of tests and employing discrete “one expectation” tests. Personally I believe it is also more expressive of the intention of the test, all the more when you build blocks for required_attribute or nonnegative_attributes.
Context is King
The “(Over)Use Context” tip is spot on. Early on in my use of rspec I tended to have code littered with one-off tests. That is, most of the test code was duplicated except for one or two key parameters. By using contexts, specifically nested contexts, you can reduce the duplication and bring sharper focus to what is varying. Use the outer context to group all those unchanging parameters and let the outer context description identify how they are common. Then, use the nested context to explicitly declare what is varying. Importantly, the before block in each context should set up the test scenario as described by the context.
As an additional example, consider a class with a state machine. The state machine begins in the ‘pending’ state and then moves to the ‘active’ state only when the ‘activate’ event is triggered. You might begin testing this scenario as follows.
describe Thing do
before :each do
@valid_attributes = { ... }
end
it "should create an instance given valid attributes" do
lambda{ Thing.create @valid_attributes}.should change(Thing, :count).by(1)
end
it "should default to the pending state upon creation" do
@thing = Thing.create @valid_attributes
@thing.should be_pending
end
context "pending instance" do
before :each do
@thing = Thing.create @valid_attributes
end
it "should transition to 'active' when activated" do
lambda{ @thing.activate }.should change(@thing, :state).to('active')
end
end
end
From the flow of the code we can see just what was described: if the attributes are valid we can create a new instance and that instance will correctly default to the pending state and from the pending state we can activate the instance and it will become active. Perfect. Until. Until? Yes, until you have to modify the state machine. Let’s say you’ve run with this code for a while and found a bot probing your application. To filter out bot created things you decide to begin with an ‘unconfirmed’ state that requires an email confirmation of some type.
What happens with the tests in this new case? The second test (default to pending) naturally fails. So does the third test (transition to active). Should it? Probably not.
What’s missing is that the before block did not guarantee that the testing state was what the context claimed that it was. How do you guarantee such things? Add an assertion. This step is commonly overlooked in a lot of tests that I have seen.
context "pending instance" do
before :each do
@thing = Thing.create @valid_attributes
@thing.should be_pending
end
it "should transition to 'active' when activated" do
lambda{ @thing.activate }.should change(@thing, :state).to('active')
end
end
The only change here is the addition of the assertion to the before block. When you re-run the tests, the third test about transitioning to ‘active’ will still fail. However, now the failure will indicate that the error lies with the assertion in the before block. Previously we had assumed the object was in the ‘pending’ state because it was the default state. Now we make that assumption explicit and the test report will quickly help us understand that the failure was in the test setup rather than in the state machine implementation.
Personal Preference
I don’t personally care for the shortcuts Philippe recommends. I don’t think that the use of ‘specify’ is quite as expressive as the typical it-block that invokes it in most rspec code. This matters to me somewhat because I still hold onto that thin hope that a non-technical person might be able to read an rspec and ‘get it.’ Using the more standard ‘it-block’ style also allows me to grep my rspecs for expectations. The other shortcut — using braces rather than do/end syntax for declaring tests — does not seem practical to me. In practice, only the most trivial tests are written in one line (or maybe I need more deeply nested contexts).
More Cowbell!
As a final thought, I don’t think that the
lambda{...}.should change(object, :attribute_name)
syntax is used nearly enough. I have a preference, for example, to know that calling ‘create’ actually creates a new instance rather than fails to throw an exception. That’s why in the code snippet above the first test checks to see if invoking create has changed the Thing count by one.
I have not seen the use of change() used as often with instances but it works just as well. In the third test above we’re using it to validate that the state has changed to ‘active’. You need to be careful with this; change() will fail your code if the final value is correct (ie., ended in ‘active’) but you did not change (ie., started in ‘active’ and stayed in ‘active’).
Loading...