RSpec best practice
Tags: rspec, testing, rails, tdd
Published: 2019-08-20 08:31:28

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](https://mekari.com/) 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.
```ruby
### 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"
```ruby
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.

```ruby
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
```ruby
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](http://www.betterspecs.org)

#### 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.

```ruby
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

```ruby
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
```ruby
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:
```ruby
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
- [mocking with rspec: doubles and expectations](https://semaphoreci.com/community/tutorials/mocking-with-rspec-doubles-and-expectations)
- [rspec best practice](https://github.com/abinoda/rspec-best-practices)  by abinoda
- [RSpec (Rails) Best Practices](https://medium.com/@somethvictory/rspec-rails-best-practices-c457679e4c04)
- [RSpec best practices by thoughtbot](https://thoughtbot.com/upcase/videos/rspec-best-practices)