8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Tips, tricks and how to refactor your Rspec

Last updated at Posted at 2019-12-12

As a Rails developer, I think most of you already have a chance working with Rspec. Sometimes you felt your code so hard to read and understandable but you don't know how to improve it. Even you already tried to reading some documents like BetterSpec or following Rspec Style Guide. Have you ever wondered if you can make the RSpec more readable?

view-ape-thinking-primate-33535.jpg
My feeling when I try to read my code

In this post, I will share my way to improve your RSpec from my experiment including from another author I collected here. Please comment below and together we can have better guidelines for anyone need.

This post will be separated into 3 main topics:

  • Structure of RSpec
  • How to test your Model
  • How to refactor your RSpec

But before we can go to the detail, we need to have a basic understanding of RSpec, so the easiest way is to answer the question about What is the Structure of RSpec?. You can check it here

Structure of RSpec

Overview

In this part, I will try to clear some Misunderstood and unclear things:

  • Describe vs Context
  • Before vs Let vs Subject
  • Before & Context / Subject & Describe

Describe / Context

The block below is showing how often people using both describe and context :

describe Ranking, type: :model do
  describe "ActiveModel scope" do
    describe '#available' do
      context 'valid target record is existed' do
      #test
      end
    end
  end

  describe '#notify_user' do
    context 'user allows notifying' do
      #test
    end
  end
end
  • According to the RSpec source code, context is just an alias method of describe -> No difference between these two methods
  • Using both describe blocks and context blocks together
  • Describe is to wrap a set of tests against one functionality -> For things
  • Context is to wrap a set of tests against one functionality under the same state -> For state
describe Ranking, type: :model do
  let(:user)    { create(:profile, :male).user }
  let(:ranking) { user.ranking }

  describe "ActiveModel scope" do
    describe '#available' do
      before { 100.times { create(:user, last_active_at: rand(20.days).seconds.ago) } }
      context 'valid target record is existed' do
        before do
          correct_ranking
        end

        it { expect(Ranking.available.count).to eq 100 }
      end

      context 'invalid target record has existed' do
        it { expect(Ranking.available.count).to eq 0 }
      end
    end
  end

  describe "#public instance methods" do
    describe '#notify_user' do
      context 'user allows notifying' do
        before do
          ranking.update!
          ranking.notify_user
        end

        it { expect(PushNotifications::RankingLevelJob).to have_been_enqueued }
      end

      context 'user account has been rejected' do
        let(:user) { create(:user, status: :rejected) }
        before { user.ranking.notify_user }

        it { expect(PushNotifications::RankingLevelJob).to_not have_been_enqueued }

      end
    end
  end
end

Things

Bad:

context '#calculator'

Good:

describe '#calculator'

States

Bad:

describe 'when the number is negative'

Good:

context 'when the number is negative'

Note:

  • context isn't available at the top-level
  • Organize your tests by things

Prefer to use context in:

  • Tests usually deal with permutations of state.
  • Nested state so using nested context is the better choice
  • Use describe when we have complex return values.

context naming:

  • Should not match your method names!
  • Explain why we would call the method

Bad

describe "#assign"

Good

context "assigning a new value for the number A"

Before / Let / Subject

Before vs Let vs Let!

  • let block is only executed when referenced, lazy loading. This means that let() is not evaluated until the method that is formed is run for the first time -> only run once.

  • let! blocks are executed in the order they are defined (much like a before the block). The core difference between let! and before is that you get an explicit reference to this variable, rather than needing to fall back to instance variables.

  • let! can run multiple time

  • before eagerly runs code before each example, even if the example does not use any of instance variables defined in the blocks

  • Use before for actions

  • Use let or let! for dependencies (real or test double)

Actions

Bad:

let(:dummy) do
  @calculator.initialize_number
end

Better:

before do
  @calculator.initialize_number
end

#or 

before { @calculator.initialize_number }

Dependencies

Bad:

before { @multiple_levels = [1, 3, 5] }

Better:

let(:multiple_levels) { [1, 3, 5] }

Before vs Subject

Let vs Subject

  • let defines a named variable

  • subject is the object being tested

    • is automatically based on the describe.
    • can be explicitly specified.
    • can have a name.
    • will be the target of "bare" should.
  • Both methods are lazy loading.

  • Can use subject! to instantiated eagerly (before an example in its group runs)

  • Can expect without writing subject or the name of a named subject

  • Use subject for the thing you are testing

  • Use let for đependencies

Bad:

let(:book) { Book.new(name: "The Hobbit", author: "J. R. R. Tolkien") }

Better:

subject(:book) { Book.new(name: "The Hobbit", author: "J. R. R. Tolkien") }

Before & Context / Subject & Describe

Before & Context

  • In the official documentation don't have any mention about the relative about before & context, but when trying to understand the purpose of each method. We can see before aligns with the context
    • context is used for the state.
    • before lists the actions to get to that state.
context "the number of books can be borrowed has been maximized"
  before do
    10.times { book_basket << Book.new }
  end
end

Subject & Describe

  • Like the before & context, the relation between subject & describe is the same. subject aligns with the describe
    • describe is used for things
    • subject specifies the thing to test

At the top-level

describe Calculator do
  subject(:cal_target) { Calculator.new(a: 5, b: 10) }
end

Can be used as nested level

describe Calculator do
  subject(:cal_target) { Calculator.new(a: 5, b: 10) }

  describe 'the result after calculate' do
    subject(:cal_result) { cal_target.calculate }
  end
end

To be continued in the next part

8
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?