Dry your RSpec Tests with Shared Examples
February 04, 2017 | 7 mins readWhen I refactored a project a few weeks ago, I spent most of my time writing specs. After writing several similar test cases for some APIs, I started to wonder whether I might be able to get rid of a lot of this duplication.
So I threw myself into reading up on the best practices for DRYing up tests (Don’t Repeat Yourself). And that’s how I came to know of shared examples
and shared contexts
.
In my case, I ended up using shared examples. And here’s what I’ve learned so far from applying these.
When you have multiple specs that describes similar behavior, it might be better to extract redundant examples in shared examples
and use them in multiple specs.
Suppose you have two models User and Post, and a user can have many posts. One should be able to view list of users and posts. Creating index action in users and posts controller will serve the purpose.
First write specs for index action of users controller which has responsibility of fetching users and rendering them with proper layout, and then write enough code to make tests pass.
Typically index action of any controller fetches and aggregates data from few resources if required, and adds pagination, searching, sorting, filtering or scoping. Finally, all these data is presented to views via html, or JSON or XML in apis. To simplify example, index actions of controllers will just fetch data and show them via views.
Same goes for index action of posts controller:
Rspec tests written for both users and posts controller are very similar. In both controllers,
- Response code – should be ‘ok’
- Both index action should render proper partial or view - in our case
index
- The data to be rendered, such as posts or users
Let’s DRY specs of index action by using shared examples.
Where to place
I like to place shared examples defined inside specs/support/shared_examples directory so that all shared example related files are loaded automatically.
To read about other commonly used conventions to put shared examples from here: shared examples documentation
How to define
Index action should respond with 200 success code (ok) and render index template.
Apart from it blocks, before and after hooks, let blocks, context and describe blocks can also be defined inside shared examples. I personally prefer to keep shared examples simple and concise, and don’t add contexts and let blocks. Shared examples block also accepts parameters, which I will cover in subsequent sections.
How to use
Adding include_examples "index examples"
to users and posts controller specs includes “index examples” to our tests.
You can also use it_behaves_like
or it_should_behaves_like
instead of include_examples in this case. it_behaves_like
and it_should_behaves_like
are actually aliases and works in same manner, so they can be used interchangeably. However, include_examples
and it_behaves_like
are different.
As stated in documentation,
- include_examples` - includes the examples in the current context
it_behaves_like
andit_should_behave_like
- includes the examples in nested context
How does it make any difference?
RSpec documentation gives proper answer:
When you include parameterized examples in the current context multiple times, you may override previous method definitions and last declaration wins.
So, when you face situation when parameterized examples contains methods that conflicts with another method in same context, you can replace include_examples
with it_behaves_like
method. This will create nested context and avoid this kind of situations.
How to pass parameters to shared examples
Look at following line in users controller specs,
it { expect(assigns(:users)).to match(User.all) }
and it { expect(assigns(:posts)).to match(Post.all) }
from posts controller.
Now, controller specs can be refactored further by passing parameters to shared example as below:
Now, make following changes in users and posts controller specs.
Now controller specs look clean, less redundant and more importantly, DRY. Furthermore, “index examples” can serve as basic structure for designing index action of other controllers.
Conclusion
By moving common examples into separate file we can eliminate duplication and more importantly, we can improve consistency of our controller actions throughout the application. This is very useful in case of designing APIs, as we can use existing structure of RSpec tests to design tests and create APIs that adhere to common response structure. Mostly, I work with APIs and use shared examples to provide me common structure to design similar APIs.
The article was also published on FreeCodeCamp publication, for which I am thankful of Quincy Larson.
Feel free to share how you DRY up your specs and use shared examples.