RSpec best practice

bambangsinaga

Parulian Sinaga

Posted on August 20, 2019

RSpec best practice

I use RSpec in my daily work. It’s really hard to overemphasize how helpful it is and how much easier becomes your life if you have good specs coverage. But its outstanding flexibility enables many ways to make your specs awful: horribly slow, over-bloated, even non-readable sometimes. I do not want to teach you BDD and RSpec here, but instead I will give you some ideas how to improve your specs quality and increase efficiency of your BDD workflow.

Here are some tips how we are at Mekari structure test code. These should help you to keep your tests well structured, easy to read, comprehend, and DRY.

1. describe

Be clear about what method you're describing. For instance, use the ruby documentation convention of . when referring to a class method's name and # when referring to an instance method's name.

### Bad example
describe 'the authenticate method for User' do
describe 'if the user is an admin' do

### Good example
describe '.authenticate' do
describe '#admin?' do

2. context

context starts either with "with" or "when", such "when status is pending"

describe '#status_badge' do
  context 'returns css class based on status' do
    context 'when status is pending' do
      let(:request_status) { 'pending' }

      it 'returns css for grey badge' do
        expect(subject.status_badge).to eql 'c-badge--grey'
      end
    end

    context 'when status is approved' do
      let(:request_status) { 'approved' }

      it 'returns css for green badge' do
        expect(subject.status_badge).to eql 'c-badge--green'
      end
    end
  end
end

3. it

it describes a test case (one expectation) and specify only one behavior. Multiple expectations in the same example are signal that you may be specifying multiple behaviors. By specify only have one expectation, helps you on finding possible errors, going directly to the failing test, and to make your code readable.

Anyway, in tests that are not isolated (e.g. ones that integrate with a DB, an external webservice, or end-to-end-tests), you take a massive performance hit to do the same setup over and over again, just to set a different expectation in each test. In these sorts of slower tests, I think it's fine to specify more than one isolated behavior.

it { is_expected.to belong_to(:job_title).class_name('JobTitle').optional }
it { is_expected.to belong_to(:template).class_name('Approval::Template') }

# or

it 'has relations' do
  is_expected.to belong_to(:job_title).class_name('JobTitle').optional
  is_expected.to belong_to(:template).class_name('Approval::Template')
end

4. subject

subject makes way clearer about what you're actually testing and helps you stay DRY in your tests

describe Book do
  describe '#valid_isbn?' do
    subject { Book.new(isbn: isbn).valid_isbn? }

    context 'with a valid ISBN number' do
      let(:isbn) { 'valid' }

      # ...
    end

    context 'with an invalid ISBN number' do
      let(:isbn) { 'invalid' }

      # ...
    end
  end  
end

There is more what you can do with RSpec and you can read them on betterspecs

5. Mocking

mocking is interesting and usually we're doing mocking when the scenario which we want to test require another service.

You may mock just everything so your spec will never hit the database or another service. But, this is something wrong. When your model code changed or the initiliaze method of service you are call changed, your code will break without get failing specs before merge to production.

By mocking objects in advance, you can allow yourself to focus on the thing that you’re working on at the moment. Let's say that you are working on a new part of the system, and you realize that the code you're currently describing and implementing will require two new collaborating objects. Using mocks, you can define their interfaces as you write a spec for the code you're currently working on.

That way, you maintain a clean environment by having all your tests pass, before moving on to implement the collaborating objects. Without mocks, you'd be required to immediately jump to writing the implementation for the collaborating objects, before having your tests pass. This can be distracting and may lead to poor code design decisions. Mocking helps us by reducing the number of things we need to keep in our head at a given moment.

Since you're able to mock, remember so as not to mock everything to green your specs or to make your spec never hit the database. This is something wrong. When your object code changed or the initialize method of object you're call changed, your code will break without get failing specs before merge to production.

class OvertimeRequest do
  #...
  def allow?
    current_company.active_subscribe?
  end
end

RSpec.describe OvertimeRequest do
  before do
    allow_any_instance_of(Company).to receive(:active_subscribe?).and_return true
  end

  it ... do
  #..
end

6. Custom matchers

Rspec has many useful matchers, we already used be true, be false, and so on. Sometimes, when expecting given values, we repeat the same code over and over again. Let's consider the following example

RSpec.feature 'some feature', type: :feature do
  it 'saves data' do
    #..

    expect(page).to have_css('.c-alert--success', text: 'Saved successfully', visible: :all)
  end

  it 'returns errors' do
    #..

    expect(page).to have_css('.c-alert--failed', text: 'Failed to save', visible: :all)
  end
end

# -- another 100 example to check flash message

wouldn’t be easier if you could write just this

expect(page).to have_flash_message("Saved successfully", type: :success)

If you've consistent logic across the whole app, it's better to create such custom matcher. To create such matcher you've to create matchers.rb file (name it as you want) in spec/support directory and put there your matcher definition:

RSpec::Matchers.define :have_flash_message do |message, opts = {}|
  match do |container|
    css_class = opts[:type].present? ? ".c-alert--#{opts[:type]}" : ".c-alert"

    expect(page).to have_css(
      css_class,
      text: message,
      visible: :all
    )
  end
end

The last step is to require your matcher by including require 'support/matchers' in the spec_helper.rb file.

resources

Some links that can help

💖 💪 🙅 🚩
bambangsinaga
Parulian Sinaga

Posted on August 20, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

RSpec best practice
rspec RSpec best practice

August 20, 2019