RSpec Best Practices
I’ve been using RSpec on and off for the past 2 years. I’ve learnt that it’s easy to write tests that are bloated, slow and don’t offer any value. I’ve also learnt a few tricks and best practices that will make your RSpec life a bit easier.
Some of these might be straightforward while others might be debatable – the only common theme is that they’re all my opinion.
1. Describe your Methods
Unit tests usually involve testing a single method on a single class. It is therefore very important that we describe the method we are testing in a consistent fashion. The Ruby documentation already sets a standard here – use . when referring to a class method and # when referring to an instance method.
This may seem like a simple thing, but it’s great for readability.
2. Use subject
When I started using RSpec I used to write tests like this:
This certainly works, but it’s very verbose – especially since we’re only really testing 2 properties. Then I discovered subject, which makes this much cleaner.
Do keep in mind that subject can make your code cleaner, but you don’t need to use it everywhere. Sometimes regular it blocks are still the best approach. You especially shouldn’t be calling subject explicitly.
There is also the concept of an implicit subject – basically if you don’t specify any subject the subject will be an instance of whatever is specified in the describe block (instantiated with the default constructor). This is very useful if you want to test some defaults on the object under test.
3. Use let and let!
The problem with @instance variables is that they spring into existence whenever they are referenced. So if we accidentally type @prodect instead of @product this will simply created a nil reference which can give us false positives.
A better approach is to use let:
Now we will get an error if we mistype the variable name. Let also has other advantages:
- It will return the same reference when use multiple times in the same example, but not across examples
- It’s lazy loaded so you won’t waste time initializing variables that you don’t need
Of course, having variables lazy loaded is not always ideal – if you want to force variables to be created you can use the alternative let! version.
4. Use specify
There are some scenarios where the it syntax really becomes a burden – the assertion is very easy to read, but it’s difficult to write a description that doesn’t just repeat the assertion. You could obviously leave the description out, but that doesn’t read any better.
A better approach is to use specify – which is simply an alias to it, but can really help readability.
5. Use context
As the name implies, context is simply another block (similar to describe) which helps you to organize your tests into logical blocks and improve readability. You can really create some nicely structured tests if you take advantage of the way let and subject blocks interact.
6. Use factories
This is the first ‘best practice’ which is highly debatable. Steve Klabnik wrote an excellent article on why factories can be a bad idea and how it can slow down your test suite. It might not seem like a big deal when you only have 200 or 300 tests and your entire suite runs in 30 seconds, but once you get to a point where your entire test suite takes 10 or 15 minutes to run it gets incredibly painful.
So keeping that in mind, factories can still be useful for initializing default objects. For example, you might want to test something on your OrderService class. An order is always associated with a product, so you need to create a valid product.
Great, we’re testing our service method without any need for a factory (or hitting the database). However, a month later our Product class changes slightly – we now require all products to have a price. We write a failing test for the Product validation, we add the validation to the Product class, run our test suite and… 27 failures. Uh-oh, everywhere that we’re instantiating Product in our tests needs to change – we need to add a price everywhere.
This is the kind of scenario were I find factories very useful. You can avoid factories if you really want, but I think you’re opening yourself up to a large amount of pain.
I do agree with Steve that the speed of your test suite is important and hitting the database should be avoided, but of course you can always hit the database without factories as well. Bottom line: use factories, but with caution.
7. Use matchers
RSpec has a pretty complete set of matchers which improves the readability of your code as well as improving the error message for failures.
Here are the results of these 2 failures:
It’s pretty easy to see which of these error messages are more readable. Any boolean property can be used as a matcher, which is why you can write code like this:
Many libraries include additional matchers and you can even write your own custom matchers.
8. Use shared examples
Shared examples is a feature which is very useful for removing duplication between tests. I have found this to especially useful when I have two models with similar functionality. (Example from RelishApp documentation)
You can also pass parameters to the shared_examples block which allows you very strong flexibility in implementing shared specs.
9. Use expect
The expect syntax was introduced in RSpec 2.11 and is very useful in scenarios where you are not using the subject syntax.
Again, this is another tool to improve the readability of your code. The choice between expect and should simply boils down to readability.
Additional Resources
If you are completely new to RSpec or Rails there are two books which I can recommend.
There are obviously many other patterns which can be considered ‘best practices’ within testing – how much to test, how much to mock/stub, etc – but these aren’t really specific to RSpec, which is what I’ve tried to focus on here.
Happy coding.