(My) RSpec Best Practices and Tips

After a year using RSpec, I’m happy to share “(My) RSpec Best Practices and Tips”. Let’s make your specs easier to maintain, less verbose, more structured and covering more cases!

Use shortcuts specify {}, it {} and subject {}

You think RSpec is verbose? In case your code doesn’t need any description, use a specify block!

1
2
3
it "should be valid" do
  @user.should be_valid
end

can be replaced with

1
specify { @user.should be_valid }

RSpec will generate a nice description text for you when running this expectation. Even better, you can use the it block!

1
2
3
4
describe User do
  it { should validate_presence_of :name }
  it { should have_one :address }
end

In case the subject is the not the class described, just set it with the subject method:

1
2
subject { @user.address }
it { should be_valid }

Start context with ‘when’/‘with’ and methods description with ‘#’

Have you ever get a failed test with an incomprehensible error message like:

1
User non confirmed confirm email wrong token should not be valid

Start your contexts with when and get nice messages like:

1
User when non confirmed when #confirm_email with wrong token should not be valid

Use RSpec matchers to get meaningful messages

In case of failure

1
specify { user.valid?.should == true }

displays:

1
2
3
'User should == true' FAILED
  expected: true,
  got: false (using ==)

While

1
specify { user.should be_valid }

displays:

1
2
'User should be valid' FAILED
  expected valid? to return true, got false

Nice eh?

Only one expectation per it block

I often see specs where it blocks contain several expectations. This makes your tests harder to read and maintain.

So instead of that…

1
2
3
4
5
6
7
8
describe DemoMan do
  it "should have expected attributes" do
    demo_man = DemoMan.new
    demo_man.should respond_to :name
    demo_man.should respond_to :gender
    demo_man.should respond_to :age
  end
end

… do this:

1
2
3
4
5
6
7
8
9
10
11
describe DemoMan do
  before(:all) do
    @demo_man = DemoMan.new
  end

  subject { @demo_man }

  it { should respond_to :name   }
  it { should respond_to :gender }
  it { should respond_to :age    }
end

(Over)use describe and context

Big specs can be a joy to play with as long as they are ordered and DRY. Use nested describe and context blocks as much as you can, each level adding its own specificity in the before block. To check your specs are well organized, run them in ‘nested’ mode (spec spec/my_spec.rb -cf nested). Using before(:each) in each context and describe blocks will help you set up the environment without repeating yourself. It also enables you to use it {} blocks.

Bad:

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
describe User do

  it "should save when name is not empty" do
    User.new(:name => 'Alex').save.should == true
  end

  it "should not save when name is empty" do
    User.new.save.should == false
  end

  it "should not be valid when name is empty" do
    User.new.should_not be_valid
  end

  it "should be valid when name is not empty" do
    User.new(:name => 'Alex').should be_valid
  end

  it "should give the user a flower when gender is W" do
    User.new(:gender => 'W').present.should be_a Flower
  end

  it "should give the user a iMac when gender is M" do
    User.new(:gender => 'M').present.should be_an IMac
  end
end

Good:

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
describe User do
  before { @user = User.new }

  subject { @user }

  context "when name empty" do
    it { should not be_valid }
    specify { @user.save.should == false }
  end

  context "when name not empty" do
    before { @user.name = 'Sam' }

    it { should be_valid }
    specify { @user.save.should == true }
  end

  describe :present do
    subject { @user.present }

    context "when user is a W" do
      before { @user.gender = 'W' }

      it { should be_a Flower }
    end

    context "when user is a M" do
      before { @user.gender = 'M' }

      it { should be_an IMac }
    end
  end
end

Test Valid, Edge and Invalid cases

This is called Boundary value analysis, it’s simple and it will help you to cover the most important cases. Just split-up method’s input or object’s attributes into valid and invalid partitions and test both of them and there boundaries. A method specification might look like that:

1
2
3
4
5
6
7
8
9
10
describe "#month_in_english(month_id)" do
  context "when valid" do
    it "should return 'January' for 1" # lower boundary
    it "should return 'March' for 3"
    it "should return 'December' for 12" # upper boundary
  context "when invalid" do
    it "should return nil for 0"
    it "should return nil for 13"
  end
end

I hope this will help you improve your specs. Let me know if I missed anything! :)

You could also be interested in (My) Cucumber best practices and tips or rspec-set a little gem that helps you speeding up your model specs.

Comments