12/08/2018, 18:11

Cleaner and Maintainable RSpec

In this article I would like to focus on some random gotchas I had lately when working with RSpec that may help you out in avoiding mistakes and DRY up your specs: 1. Understand the difference between before(:each) and before(:all) The short story: if we can't be sure about what to use, always ...

In this article I would like to focus on some random gotchas I had lately when working with RSpec that may help you out in avoiding mistakes and DRY up your specs:alt

1. Understand the difference between before(:each) and before(:all)

The short story: if we can't be sure about what to use, always use before (:each).

The long story is the before(:each) block runs before every spec in a group and is cleaned up right after(the transaction is rolled back) whereas the before(:all) block runs ONCE before all specs in a group, however it is not rolled back! that means the effects of the before(:all) block on the database will remain between specs . This is usually something we dont want since we want to clean fixtures data between specs. It can cause surprising test failures especially since tests are run in random order (sometimes your test suite will pass, sometimes it won’t). Due to this reason I’d recommend to stick to before(:each) if you’re just starting with RSpec.

2. Use describe and context to group and DRY common specs

Context and describe blocks are great since they make for more readable specs and allow you to group common setup code to be shared for all specs in the block.

To explain this let’s say we are testing our OrdersController in our e-commerce website. We have two specs for the controller create method and in both cases our user is unsigned. The content of the specs themselves is not really important for this discussion.

We can see that several things can be improved: the common setup code (signing out the user) is repeated in both specs, and despite sharing common functionality there is no way to tell these specs are related (both relate to unsigned user context).

We improve this by: (a) creating a context called “Unsigned” (b) creating a describe block for the create method.

Describe works exactly the same as context but is meant to describe a functionality (like a controller’s method — create) while context should refer to a state within that functionality (like an unsigned user).

Hopefully it’s clear why this is better: I really like that the describe and context blocks already explain what is going on : we are testing a controller’s create method and specifically an unsigned user functionality. By using before blocks inside the context we can run common set up code for all specs (in this case signing out the user). Moreover, our spec names became super small (“is redirected to login”) since they are already described by the context and describe block: I feel this is easier to reason about for people reading your specs, and eventually for you. And finally, it allows us to create structure in our test suite and remain consistent.

3. Avoid using allow_any_instance_of for mocking

allow_any_instance_of and it’s look alike expect_any_instance_of are an RSpec way to mock methods in instances. While quite handy, they are alas considered legacy and discouraged . Let’s say we have a GithubReposController’s index method, which calls a method to fetch popular repos.

Now when we want to test it, we would want to mock the HTTP call to fetch popular repos (if it’s unclear to you why I suggest you read more on mocking).

def index 
  @github_client = GithubClient.new(secret_a, secret_b)
  @repos = @github_client.fetch_popular_repos()
end

Since we have no control over the creation of GithubClient, it’s not quite clear how can we mock it? allow_any_instance_of looks like a great candidate:

it "returns popular github repos" do
  allow_any_instance_of(GithubClient).to receive(:fetch_popular_repos).and_return({id: 1, name: 'ruby-on-rails'})
  get :index
  expect(response.body).to include('Ruby on Rails')
end

While this is fine, let’s rewrite the spec without using allow_any_instance_of and using doubles instead: While a bit less concise we are at least not using a legacy RSpec feature!

This is just scratching the surface of RSspec but hopefully it will help some of you to write more readable and maintainable specs!

0