11 Steps To Make Your RSpec Specs Awesome

Preface

Everyone agrees that writing clean ruby code is essential. There are many blog posts about refactoring your ruby code to make it more readable and maintainable. As tests become a major part of our programming routine, I feel there is a large place for improvement in the way we write and maintain our test-suites. Many ruby projects consist of more rspec lines than actual ruby code, and that requires us to treat our test code with as much care as we treat our actual ruby code, if not even more. Writing specs may be considered by programmers as a burden or a chore if not done the right way. That may cause some programmers to write less specs for their code or to even totally give up on it. Luckily, RSpec provides us with all the tools necessary to create efficient, great looking and fun-to-write specs for our code. I’d like to go over a process of refactoring a test-suite written in RSpec for a small library I’ve recently written in FTBpro.com.

The Domain

In FTBPro we have posts written by users, and a dynamic set of reaction templates that allows the user to react to that post. A post has a locale and a type, reaction templates has a locale and an array of types which they fit to. When a new post is created, we want to attach the most suitable reaction template to it:

  1. It must have the same locale as the post.
  2. Its types array must include the post’s type.
  3. From the set of matching reaction templates by rules 1 and 2, choose the one which was created the latest.

I came out with the following interface to do this job:
FindReaction.for(post).attach
While
FindReaction.for(post).reaction
holds the reaction which was found – I mainly exposed this for tests purposes.

Now the funny part about this blog post is that I’m not going to post here the code which implemented this logic, we shouldn’t even care about it. What I’m going to do is to present you with a messed up test suite for this logic, and lead you through the process of making it a blast.

This test suit contains 4 simple specs which describe the behavior we expect from FindReaction:


describe FindReaction do
#Spec 1
it "should return no reaction template when there are no reaction templates" do
post = create :post, locale: "en", type: "basic"
reaction = FindReaction.for(@post).reaction
reaction.blank?.should == true
end
#Spec 2
it "should return no reaction template if reaction templates exist, but non fitting" do
post = create :post, locale: "en", type: "basic"
reaction1 = ReactionTemplate.create locale: "de", subtypes: ["basic", "top_x"]
reaction2 = ReactionTemplate.create locale: "en", subtypes: ["slideshow", "top_x"]
reaction = FindReaction.for(@post).reaction
reaction.blank?.should == true
end
#Spec 3
it "should return the right reaction if it exists" do
post = create :post, locale: "en", type: "basic"
reaction1 = ReactionTemplate.create locale: "en", subtypes: ["basic", "top_x"]
reaction2 = ReactionTemplate.create locale: "en", subtypes: ["basic", "top_x"]
reaction3 = ReactionTemplate.create locale: "en" , subtypes: ["slideshow", "top_x"]
reaction = FindReaction.for(@post).reaction
reaction.blank?.should == false
reaction.should eq reaction2
end
#Spec 4
it "should change posts reaction_template when attaching it" do
post = create :post, locale: "en", type: "basic"
post.reaction_template.nil?.should == true
reaction = ReactionTemplate.create locale: "en", subtypes: ["basic", "top_x"]
FindReaction.for(post).attach
post.reaction_template.nil?.should == false
post.reaction_template.should eq reaction
end
end

Although these specs pass they’re not optimal. Let’s follow these rules of thumb to make this spec a blast!

Rule #1: Keep your specs DRY – use before blocks

The first line of each spec is identical – we create a post. It should be extracted to a before block so we don’t have to mention it over and over again.

Rule #2: Use before :all instead of before :each when possible

Why should we create the post before each spec runs? If you’re not changing your model’s attributes between specs, you should definitely use before :all.
A note on this: unlike before :each blocks which are wrapped in a DB transaction and thus getting rollbacked after the spec is executed, before :all blocks have to be cleaned explicitly so that you don’t fill your test DB with garbage.

Rule #3: Use build instead of create when possible

We’re using FactoryGirl to create a post. Creating is an expensive operation: The post has to go through validations, we save it to the DB and in many cases it requires other models to be created as well. For example, if part of a post validation says it has to have a user than we’ll create a user as well. By building a post instead of creating we save all this time and get the same outcome.

Rule #4: Use describe to create nested specs groups. describe should relate to an operation or an object.

We’re actually testing here 2 different methods of FindReaction. Specs 1-3 describe the reaction method while spec #4 describes the attach method. Each of these methods should have its own describe block.

Rule #5: Use variables instead of constants to keep your specs from becoming too fragile.

In lines 19-21 we create reaction templates and we want their locale to match the created post’s locale. Instead of using the string “en” again and again we should use post.locale
This makes your spec less fragile so that if someday someone changes the post’s locale, he won’t have to change the reaction templates creation as well.

Rule #6: Use RSpec’s magnificent be_ syntactic sugar

RSpec has many syntactic sugars that makes your specs more readable. Let’s look at line 6 for example:
reaction.blank?.should == true
When an object reacts to some_method_name? RSpec allows you to write the test in the following way:
object.should be_some_method_name
Or in our case:
reaction.should be_blank

Rule #7: Use create! and save! instead of create and save

When creating or saving models to your DB, use create! and save! instead of create and save.
The difference is that the bang version of the methods throws an exception if the operation fails, while the non-banged versions just return false. I wasted a lot of time debugging failed specs only to find out the model was not saved or created due to validation or another problem. By using create! and save! you’ll be sure that the model was created successfully and it exists for you to test it, otherwise an exception will be thrown and you’ll know about it immediately.

Midway

After applying these  7 simple rules our spec following already looks much better:
https://gist.github.com/erez-rabih/6698414
You might think to yourself “OK, this looks great”, but I’ve got a surprise for you: The interesting part is only ahead of us. Let’s take the extra leap to improve our specs even more.

Rule #8: If/When rule of thumb

When there is a “if” or “when” in your it “should …” clause, there’s a hidden context in it.
Looking at spec #2 our it clause looks like this:
it "should return no reaction template if reaction templates exist, but non fitting"
There’s an if in it, and that’s exactly what we’re looking for. If or when mean you’re describing a state. These states should be extracted to a context block with a before block describing the state. After applying this rule spec #2 should look like this:
https://gist.github.com/erez-rabih/6698523

We should apply the same to specs 1,3 and 4

Rule #9: Use subject/it clauses to keep your specs DRY’er and cleaner

We can easily identify that specs 1,2 and 3 all test the same result: FindReaction.for(@post).reaction
This can be easily extracted into subject {FindReaction.for(@post).reaction} phrase which does miracles to our test code.

Rule #10: One assertion per test case

I like to have only one assertion in each spec. This way if the spec fails, you immediately know what went wrong and don’t even have to look at the detailed output of the failure. It also keeps your specs clean and focused on what they’re supposed to test. Spec #3 has two assertions in it, in the first we check that a reaction is found and in the second we check what specific reaction was found. These are two completely different assertions and deserve different specs. The one assertion per spec rule is easily achieved with combination of Rule #9, we’ll see why right away.

Rule #11: Use RSpec’s expect {} block to describe a change in the state of an object

In spec #4 we test that the post’s reaction_template has changed from nil to the attached reaction. RSpec has a nice and clean way to describe these kind of state changes:
expect { some_operation }.to change{something}.from(initial_value).to(final_value)
It is much cleaner and more readable this way.

This is the result after applying rules #8-#11 to our spec:


describe FindReaction do
before :all do
@post = build :post, locale: "en", type: "basic"
end
before :each do
@post.reaction = nil
end
describe :reaction do
subject { FindReaction.for(@post).reaction }
#Spec 1
context "there are no reaction templates" do
it {should be_blank }
end
#Spec 2
context "reaction templates exist, but non fitting" do
before :each do
reaction1 = ReactionTemplate.create! locale: "de", subtypes: ["basic", "top_x"]
reaction2 = ReactionTemplate.create! locale: "en", subtypes: ["slideshow", "top_x"]
end
it {should be_blank }
end
#Spec 3
context "fitting reaction templates exist" do
before :each do
@reaction1 = ReactionTemplate.create! locale: @post.locale, subtypes: [@post.type, "top_x"]
@reaction2 = ReactionTemplate.create! locale: @post.locale, subtypes: [@post.type, "top_x"]
@reaction3 = ReactionTemplate.create! locale: @post.locale, subtypes: ["slideshow", "top_x"]
end
it {should_not be_blank}
it {should eq @reaction2}
end
end
describe :attach do
#Spec 4
context "attaching a reaction to post" do
before :each do
@reaction = ReactionTemplate.create! locale: @post.locale, subtypes: [@post.type, "top_x"]
end
it "should change posts reaction template" do
expect {
FindReaction.for(@post).attach
}.to change{@post.reaction_template}.from(nil).to(@reaction)
end
end
end
end

Conclusion

So what have we earned in this refactoring process? I’ll point the main advantages of the new code:

  1. Our test-suite runs faster
  2. Our test file is more readable
  3. The failure output provided by RSpec is better
  4. We’re not repeating ourselves between specs
  5. It is extremely easy to extend the specs to describe new features
  6. I find it much more enjoyable writing specs that looks cool (And you must admit, it does look cool).

I know that even this code can be optimized and refactored, but I had to stop somewhere 🙂
I’ll be glad to hear your insights and thoughts.

* This post was also featured on FTBpro’s Tech Blog

18 thoughts on “11 Steps To Make Your RSpec Specs Awesome

  1. i’m with Guoliang on this. Too much logic and flow in specs is hard to follow. I don’t want maintain another separate program to test my program.

    also i would ask what is the meaning of :each block inside context in the final spec #2 and #3
    thanks

    1. The before :each blocks are there to set up the data according to the context for the spec in the it {} block. This is needed because when using it {} in the proc/lambda form this is the only way to set up data before the subject {} is evaluated.

  2. I find it good practice to run rspec –format=documentation and read the output. Output should be easy to read and understand. It’s also easy way to spot if you aren’t testing implementation by accident.

    For this reason I always “name” context as “when …”, “as …”, “with….”, ie. context “when logged in as admin”.

  3. Nice writeup! I learned a lot of tricks. However the spec before applying those rules gives me a feel of simplicity. The refactored spec is so complex, I could not skip one line of the whole file before I know what’s going on in a specific spec. There are so many concepts and layers (describe, before, context, subject etc)? Who is going to make sure that the way we tie them together is doing what we want?

    1. You raise a nice point. Although the initial form of the spec is really simple to read you have to take into account that what I’ve shown here is a small portion of regular test suits which often consist of thousands of lines. If you won’t extract test code to before blocks, describe and context scopes your test suit is doomed to be unusable as it grows in scale:
      1. It will become bloated with repeating code.
      2. It will become so slow that the developers will not run the specs because they will slow their development instead of making it faster.
      3. It will become hard to change to fit changing requirements.
      4. It will become fragile so that specs will fail although the behaviour did not change.
      5. It will become hard to extend because once you don’t have the hierarchy of describe / context blocks every developer will just create his own it clause with a full description of the situation.

      You have to remember that although these specs look good on small scale, they won’t on real-life situation.

  4. You should have a clear warning on using before(:all). Accepted wisdom on this is that tests should setup their own artifacts otherwise you can introduce side-effects where one test breaks another test but you won’t know what test broke it. It’s painful.

    Of course they are not proper unit tests anyway as they interact with the database but that’s another story.

    1. Hi Phil, thank you for your comment. A few thoughts of mine:
      1. In step #3 I explicitly encourage using build instead of create and accordingly replace the before :all block with a build method call. This means that no DB changes are made in this before :all block.
      2. In rule #2 I do issue a warning about using before :all in which it is explicitly mentioned that unlike before :each blocks which are wrapped in a DB transaction and thus are rollbacked, before :all blocks must be cleaned by the spec’s writer in an after :all block. I do agree that I should have made this warning more prominent.
      3. From my humble experience, using before :all blocks in the main describe scope works pretty well. As long as you remember it is your responsibility to clean up whatever mess you did, things should go pretty slick.

      Don’t you agree that before :all blocks, when used cautiously, can save a lot of repeatedly ran code?

      1. When a test mutates an attribute on an object that was defined in a before(:all) block, all the later tests in the scope see an object that behaves differently from the clean one they would have seen from a before(:each) block. As Phil says, this leads to unexpected behavior.

        In your code (final version), you reset the reaction attribute (on @post) every time, but any other attributes don’t get reset. In your last test, a different attribute, reaction_template, gets changed. If you copy that test and run it twice in that block, I think the second one will fail because the reaction_template attribute won’t start out as nil (unless the reaction and reaction_template attributes are entangled in some way that I’m not aware of).

        It’s easy to miss an attribute that needs to be reset explicitly every time, especially if you’re a developer coming in later and adding a new test that mutates a new attribute on the same object. Better to remove the need in the first place. I agree with ruby developer that let(…) { … } is a good option that gives you the best of both worlds, creating the object only for the tests that need it.

        If some specific before(:each) block or let statement turns up as a serious performance bottleneck when you profile your tests, then sure, before(:all) is an option you could use very cautiously for that particular case after exhausting safer alternatives. But the performance gains should be weighed against the time that will be spent dealing with hard-to-debug issues arising from shared state (and the higher risk of bugs in production if the tests aren’t checking what you think).

        1. These are some excellent drawbacks examples to think about before going the before :all road. No doubt it should be used only when the before :each block becomes a performance bottleneck in your test suit. Thanks for elaborating on this.

    1. I agree that let is a really useful tool but you must pay attention to the difference between let and before :all
      If you do a before :all in your main describe scope it will evaluate only once of that spec file. On the other hand let evaluates on each spec.
      This means that in a spec file with 100 specs before :all will be evaluated once and the let will be evaluated again and again for each spec in which you use this let clause – might be even 100 times if you use this variable in each spec.

  5. Another tip, albeit a considerably less urgent one: Keep your spec descriptions shorter and more action-oriented by saying “changes post’s reaction template” instead of “should change post’s reaction template”.

Leave a reply to Lisa Cancel reply