RSpec(ActiveRecord)入門(ユースケース付き)

世にRSpecに関する記事は大量にあると思うので今更感ありますが、簡単に書いておきます。

基本的にModelのテストしか書きませんmm 実際にRailsで運用する際には models, features, requests テストくらいがあればいいと思ってるのと、基本的にFat modelにしておくのがbetterだと思うのでModelのテストをまずは抑えてけろ!という感じです :smiley: :sparkles:

前提

  • Rails(4系) の通常のセットアップは終わってると仮定
  • RSpec 3系
  • FactoryGirl
  • 今回は ActiveRecord::Base を継承したクラスのテスト見ますが, POROにも活かせるはず…! :triumph:

Sample: RockStar class

  • ActiveRecord::Base を継承した RockStar クラス
  • 1つのBandに所属
  • 下の test example 毎に対応するメソッド書いてるので, ここは流して :ok_woman: です
# Schema
# - name,       string,  null: false, default: ""
# - email,      string,  null: false, default: ""
# - age,        integer, null: false, default: 14
# - part,       integer, null: false, default: 0
# - status,     integer, null: false, default: 0
# - drugged_at, datetime

# app/models/rock_star.rb
class RockStar < ActiveRecord::Base
  belongs_to :band

  validates :name,   presence: true
  validates :email,  presence: true
  validates :age,    presence: true, numericality: { greater_than_or_equal_to: 14 }
  validates :part,   presence: true
  validates :status, presence: true

  after_save :send_welcome_message, on: :registration

  enum part: %i(vocalist guitarist bassist drummer)
  enum status: %i(street box dome world-wide)

  def dead?
    age > 27
  end

  def take_drug
    update(drugged_at: Time.current)
  end

  def got_fired
    update(band: nil)
  end

  def participate_in!(band)
    raise 'Invalid band!' unless band

    update(band: band)
  end

  private

  def send_welcome_message
    UserMail.welcome_message(self.email).deliver_later
  end
end

# spec/factories/rock_stars.rb
FactoryGirl.define do
  factory :rock_star do
    name "Kurt Cobain"
    sequence(:email) { |n| "star#{n}@example.com" }
    age "27"
    part "guitarist"
    status "world-wide"
  end
end

shoulda-matchers

  • validation, relation あたりのテストを書くなら Shoulda Matchers を使いましょう
# spec/models/rock_star_spec.rb
require 'rails_helper'

describe RockStar, type: :model do
  let(:rock_star) { create(:rock_star) }

  describe 'relations' do
    subject { rock_star }

    it do
      is_expected.to belong_to(:band)
    end
  end

  describe 'validations' do
    it do
      is_expected.to validate_presence_of(:name)
      is_expected.to validate_presence_of(:email)
      is_expected.to validate_presence_of(:age)
      is_expected.to validate_presence_of(:part)
      is_expected.to validate_presence_of(:status)
      is_expected.to validate_numericality_of(:age)
        .is_greater_than_or_equal_to(14)
    end
  end
end

Check attributes

  • attribute の状態に応じて結果が変わるメソッドのテスト
  • 主体となるインスタンス( let(:rock_star) ) の attribute を context 毎に切り替えてチェックするのがヨサソウ
  • 補足ですが, インスタンスメソッドなら #instance_method_name, クラスメソッドなら .class_method_name# / . で表現してます。(どこを参考にしたかは覚えてませんが…)
# A method to be tested
  def dead?
    age > 27
  end

# Test
describe RockStar, type: :model do
  let(:rock_star) { create(:rock_star) }

  describe "#dead?" do
    subject { rock_star.dead? }

    context "when :age is over 27" do
      let(:rock_star) { create(:rock_star, age: 28) }

      it { is_expected.to eq true }
    end

    context "when :age is less than or equal to 27" do
      let(:rock_star) { create(:rock_star, age: 27) }

      it { is_expected.to eq false }
    end
  end
end

Update attributes

  • attributeを更新するメソッドのテスト
  • 変更前の状態をcheck -> method 実行 -> 結果を確認, という流れでヨサソウ
# Methods to be tested
  def take_drug
    update(drugged_at: Time.current)
  end

  def got_fired
    update(band: nil)
  end

# Test
describe RockStar, type: :model do
  let(:rock_star) { create(:rock_star) }

  describe "#take_drug" do
    before do
      expect(rock_star.drugged_at).to eq nil
      rock_star.take_drug
    end

    it { expect(rock_star.reload.drugged_at).to_not eq nil }
  end

  # or 確認したいattributeを事前に明示しておくのもあり
  describe "#got_fired" do
    let(:rock_star) { create(:rock_star, band: nil) }
    before do
      rock_star.got_fired
    end

    it { expect(rock_star.reload.band).to eq nil }
  end
end

With Arguments / Check Exception

  • 引数を取って何かをするメソッドのテスト
  • 引数で渡す値を context 毎に切り替えるのがヨサソウ
  • あとついでに例外を起こした場合の raise_error の使い方も
    • 例外を起こす処理は block で囲みます ( expect { subject }.to )
    • raise_error(ExceptionName, "error message") 第二引数の error message はオプションなのでなくてもおk
  • ruby の ! の使い方ほんとにこれであってのか謎(間違ってたらごめんなさい)
# A method to be tested
  def participate_in!(band)
    raise 'Invalid band!' unless band

    update(band: band)
  end

# Test
describe RockStar, type: :model do
  let(:rock_star) { create(:rock_star) }

  describe "#participate_in!" do
    subject { rock_star.participate_in!(band) }

    context 'when valid band' do
      let(:band) { create(:band) }

      it "updates its band" do
        subject
        expect(rock_star.reload.band).to eq band
      end
    end

    context 'when invalid band' do
      let(:band) { nil }

      it "raises error" do
        expect { subject }.to raise_error(RuntimeError, "Invalid band!")
        expect(rock_star.reload.band).to_not eq nil
      end
    end
  end
end

Call other class

  • 他のクラスを呼び出すメソッドのテスト
  • 細かいテストはそっちのクラスでやればいいので, 実際に呼び出されるかどうかをチェックする
  • expect(obj).to receive(:method_name)expect_any_instance_of(obj).to receive(:method_name) を使う
    • 後者は見ての通り instance に対して :method_name メソッドが呼ばれるかどうかをチェックする
  • 先に expect().to receive() を宣言してから, 呼び出されることが期待されるメソッドを実行する. 順序を間違えないように.
  • また, メソッドがチェーン呼び出しされる場合は receive_message_chain(:method1, :method2, ...) を使う
  • 覚えること多いですね…がんばってください! :fire: :fire: :fire:
# A method to be tested
  after_save :send_welcome_message, on: :registration

  private

  def send_welcome_message
    UserMail.welcome_message(self.email).deliver_later
  end

# Test
describe RockStar, type: :model do
  let(:rock_star) { create(:rock_star) }

  describe "after_save #send_welcome_message" do
    let(:rock_star) { build(:rock_star) }

    subject { rock_star.save(options) }

    context 'without registration context' do
      let(:options) { {} }

      it "not send email" do
        expect(UserMail).to_not receive(:welcome_message)
        subject
      end
    end

    context 'with registration context' do
      let(:options) { { context: :registration } }

      it "sends email" do
        # `with` は :welcome_message に渡される引数(もしかしたらこのテストfailするかもmm)
        # ニュアンスだけ掴んでもらえれば。
        # `with` 外せばパスすると思われ
        expect(UserMail).to receive(:welcome_message).with(rock_star)
        subject
      end
    end
  end
end

おわり

  • いろいろとつっこみどころ多いと思いますが, とりあえずは「全然RSpecの書き方わからん!」って人向けなので, Rubyの書き方とかは一旦スルーしていただけると :bow:
  • テストは書こうね :wink:

Contents