Parulian Sinaga
Posted on August 20, 2019
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
Posted on August 20, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.