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!
123
it"should be valid"do@user.shouldbe_validend
can be replaced with
1
specify{@user.shouldbe_valid}
RSpec will generate a nice description text for you when running this expectation. Even better, you can use the it block!
Start your contexts with when and get nice messages like:
1
Userwhennonconfirmedwhen#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:
123
'User should == true'FAILEDexpected:true,got:false(using==)
While
1
specify{user.shouldbe_valid}
displays:
12
'User should be valid'FAILEDexpectedvalid?toreturntrue,gotfalse
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…
12345678
describeDemoMandoit"should have expected attributes"dodemo_man=DemoMan.newdemo_man.shouldrespond_to:namedemo_man.shouldrespond_to:genderdemo_man.shouldrespond_to:ageendend
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:
1234567891011121314151617181920212223242526
describeUserdoit"should save when name is not empty"doUser.new(:name=>'Alex').save.should==trueendit"should not save when name is empty"doUser.new.save.should==falseendit"should not be valid when name is empty"doUser.new.should_notbe_validendit"should be valid when name is not empty"doUser.new(:name=>'Alex').shouldbe_validendit"should give the user a flower when gender is W"doUser.new(:gender=>'W').present.shouldbe_aFlowerendit"should give the user a iMac when gender is M"doUser.new(:gender=>'M').present.shouldbe_anIMacendend
describeUserdobefore{@user=User.new}subject{@user}context"when name empty"doit{shouldnotbe_valid}specify{@user.save.should==false}endcontext"when name not empty"dobefore{@user.name='Sam'}it{shouldbe_valid}specify{@user.save.should==true}enddescribe:presentdosubject{@user.present}context"when user is a W"dobefore{@user.gender='W'}it{shouldbe_aFlower}endcontext"when user is a M"dobefore{@user.gender='M'}it{shouldbe_anIMac}endendend
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:
12345678910
describe"#month_in_english(month_id)"docontext"when valid"doit"should return 'January' for 1"# lower boundaryit"should return 'March' for 3"it"should return 'December' for 12"# upper boundarycontext"when invalid"doit"should return nil for 0"it"should return nil for 13"endend
I hope this will help you improve your specs. Let me know if I missed anything! :)