Pro Tips For Writing Better RSpec Tests

Writing ‘good code’ is always a challenge for every project. Unfortunately, we always associate it with development and not tests. What about automation test code? Ever notice that you have write more automation code than the actual lines of development code?

Improper automation code leads to slow running specs, huge LOC in your spec files and a whole lot of confusion when you open a spec file. So, how does one avoid these problems?

Here are a few guidelines that I learned from my experience:

Tip #1: Follow Rspec style guide

That was obvious, wasn’t it? As surprising as it may seem, we invariably do not follow in detail.

  • How often do you see more than one expectation in an example? bad
  • How often do you see lots of stubs and mocks in test code? bad – keep it as simple as you can.
  • How often do you see the use of context, especially in controllers? good

A lot of them are mentioned in the Style guide and we shall repeat below a few which require more detail.

Tip #2: Independent tests

We could use the word idempotent here but as a thumb rule there should not be any dependencies across specs and if we run the test multiple times in succession, it should give the same result So, it’s very important that every spec  run in clean database environment. Use database cleaner gem properly, so that you don’t end of having to seed the database before each test! So, if you don’t want roles and countries being deleted,

config.before(:suite) do
DatabaseCleaner.strategy = :truncation, {:except => %w[roles countries]}
end

Tip #3: Use expect and not should syntax

It’s just nice and clean!  Use the Transpec gem helps you to convert from should syntax to expect syntax

expect(User.all.count).to eq(1)

instead of

User.all.count.should == 1

Tip#4: Use the new syntax for object creation

Use the new FactoryGirl syntax that is supported in factory_girl4. This helps the code be clean of any harsh gem dependent syntax of class names!

create(:user)

instead of

FactoryGirl.create(:user)

Tip #5: Use let variables instead of instance variables

This makes code look cleaner. For better understanding about let read the answer given by Myron. Here is an example

describe Project do
let(:project) { build(:project) }

it "project should be valid" do
expect(project.valid?).to eq(true)
end
end

Now, when the example executes it defines a method called project if its not defined already or simply returns the value. Fast and efficient.

Tip #6: Use correct expectations for a spec

Obvious as it seems – the test we write should test the write stuff – not just make the test pass! Here is an example of making a test case pass.

context "#edit" do
it "user should be able to edit his profile" do
get :edit, id: user.id
expect(response).to be_success
end
end

So, your expectation should ideally be

describe UsersController do
context "#edit" do
#render_views
it "user should be able to edit his profile" do
get :edit, id: user.id
expect(response).to render_template("edit")
end
end
end

Here #edit is a method defined in UsersController. So I defined a context in my specs.

Do remember that by default all controller specs stub the template rendering unless and until you include render_views

Tip #7: Use shared examples.

When you want to test the same functionality across different subjects, for example different kinds of users, use shared examples.

context "#edit" do
it "admin should be able to edit his profile"
it "distributor should be able to edit his profile"
end

You can write as

context "#edit" do
shared_examples "profile_editing" do
it "user should be able to edit his profile"
end

context "admin" do
include_examples "profile_editing"
it "should be able see to list of distributors"
end

context "distributor" do
include_examples "profile_editing"
it "should not able to see list of distributors"
end
end

A word of caution – you may lost readability if you use too many shared examples in single file.

Tip #8: Use conditional hooks

Sometimes if you want to execute some piece of code only for one example but not for all examples in a context then you can use conditional hooks.

context "#update" do
before(:each, run: true) do
# do something
end

it "example1", run: :true
it "example2"
end

Now before(:each) will run only for first example but not for second example.

Tip #9: Profiling your examples

rspec gives you an option to examine how much time does a example group take to run.  Simply run specs with the –profile flag.

rspec ./spec/models/user_spec.rb:51 --profile
# output:
0.22431 seconds ./spec/models/user_spec.rb:51

Tip #10: Tagging

When you have a large test suite and you changed some code, how do you ensure that your change didn’t breaks existing functionality? One way is to run your specs locally then push then your Continuous Integration server that runs all specs. This is time consuming if you want to test only a specific set of tests.

Tag them! Run your module specified specs by using tags. For example, suppose we have a Commission feature and we written model specs, controller specs and integration specs for it. We can easily tag each file with commission:true.  So when we change something related to commissions,  we can run only commission related specs!. We can tag our scenarios as below

# spec/models/commission_spec.rb
describe Commission, commission: true do
# do something
end

rspec –tag commission
Note: We can tag entire context or a single example in the spec file. For more information please follow this link

All these tips are a results of practical problems I have faced and overcome and a result of finding out the best we can do our own testing. More suggestions and tips are welcome!

13 thoughts on “Pro Tips For Writing Better RSpec Tests

  1. Great article. It also gives a good overall look into RSpec for those of us not using it… yet.

    In Tip #5. Shouldn’t this block:

    it “project should be valid” do
    expect(project.valid?).to eq(true)
    end

    be:

    it “should be valid” do
    expect(project.valid?).to be_true
    end

    ?

    I don’t know much about RSpec, so sorry if I’m wrong.

    /Lasse

  2. This post is full of bad examples.

    1. `context “#edit”`
    Contexts are, well, contexts. Such as “user is admin”. And not “#edit”. This one should be `describe “#edit”`. Such a seemingly subtle misuse brings to the the issue #2.
    2. `it “admin is able to edit his profile”`
    That does not read well at all. Rspec is a lot about making test code read better. Otherwise why not just use minitest? `it` is about object under test (controller in this case), not the user. So your example should instead look like:
    `context “user is admin” do
    it “allows to edit profile”
    `
    3. shared_examples. This one is particularly bad. There is a code duplication and you say: I know what to do, I’ll use shared_examples. Except the duplication is still there. You test code runs twice for essentially the same functionality. This is bad for a number of reasons, such as slower test suite and more coupling.
    Whether there were no shared_examples in rspec you might have instead asked yourself a question: WHY is there a duplication? Could that be me doing something wrong? And from there it would be easier to see that actually those tests should be changed into:
    `context “user can edit profile”
    it “renders the view”

    context “user is not allowed to edit profile”
    it “redirects to home page”
    `
    and the authorization logic (which roles can do what) should be tested in isolation elsewhere. Much faster. No duplication.

    shared_examples are useful for testing different implementations of an interface (for example, Zip and Gzip could implement imaginary Archiver interface with #pack and #unpack). But I am not sure this use case outweighs the fact just how easy it is to shoot yourself in the foot with in all other cases when you don’t actually need them.

    Also, last time I checked, failure output from shared spec was pointing out to its definition, and not where it failed in particular. Makes feedback cycle one step longer.

    4. conditional hooks (the way you put it) is an anti pattern. Use `context` that clearly state the difference in test setup. So that next person who stumbles across this code knows what is going on.

    I think you guys should give minitest a go. It does not have lots of fancy stuff and that makes it easier to concentrate on what is actually important: better code.

      1. To add to this:

        5) the overuse of the word “should” in descriptions. instead of:
        describe UsersController do
        context “#edit” do
        #render_views
        it “user should be able to edit his profile” do
        get :edit, id: user.id
        expect(response).to render_template(“edit”)
        end
        end
        end

        let’s write correctly as:
        describe UsersController do
        describe “#edit” do
        it “renders edit view” do
        get :edit, id: user.id
        expect(response).to render_template(“edit”)
        end
        end
        end

        The use of should in all our descriptions isn’t DRY. This reads nicely too. “UsersController #edit renders edit view” as opposed to: “UsersController #edit user should be able to edit his profile”

        In regards to Contexts, I feel it easiest to read that as “when”. For example, “User #create when admin it creates an admin account, when non-admin it creates a non-admin account”, looks like so in code:

        describe User do
        describe “#create” do
        context “admin” do
        it “creates an admin account” do
        end
        end
        context “non-admin” do
        it “creates a non-admin account” do
        end
        end
        end
        end

        I do like some examples here!

        1) profiling is great! I will add you can just append -p, so rspec -p will print out the top 10 slowest tests and their times

        2) let is always good, there’s let! too, let’s not forget that

        3) expect vs should is a great. Besides the fact it reads better, it’s also used because .should binds itself to every object without actually having access to every object. There are times you will get errors because of this. “expect” doesn’t bind.

        I will add a few more to your list:

        11) use “described_class” when instead of using the class name. E.g. User.all == described_class.all

        12) Use “trait” in your factories

        13) use “.send” to test complex private methods

        14) use shoulda-matchers gem for brevity, especially when testing models and associations

        15) use .should_receive to catch method calls and conform to singularity of specs

        etc. Good post! Thanks for sharing.

  3. how can you write the test for mutliple traits and associations? I have a code as such

    I have this 3 factories file ,
    bank.rb has 2 traits (stdchtd, americaexpress)
    interest.rb has 2 traits (blr, mlr)
    mortgage.rb has 3 traits but is also associated to bank.rb and interest.rb

    so e.g my mortgage.rb file looks like this

    FactoryGirl.define do
    factory :mortgage_loan do
    association :interest_rate_reference, factory: :interest_rate_reference
    association :bank, factory: :bank

    trait :legacy_id_266 do
    legacy_id 266
    bank_id 1
    name “Maxi Home”
    loan_purpose “both”
    is_conventional 1
    is_islamic 0
    interest_rate_reference_id 7
    is_featured 0
    end

    trait :legacy_id_260 do
    legacy_id 260
    bank_id 1
    name “Maxi Home”
    loan_purpose “both”
    is_conventional 1
    is_islamic 0
    interest_rate_reference_id 6
    end
    end
    end

    How do i dynamically call it in my features file. I tried doing this but got an Error

    scenario “Test Database using Update Result” do
    create(:interest_rate_reference, :blr)
    create(:interest_rate_reference, :mlr)
    create(:bank, :maybank)
    create(:bank, :cimb)
    create(:mortgage_loans, :legacy_id_260)
    click_button(“Update Results”)
    end

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.