【Rails / RSpec】モックの基本とcredentialsのモックについて

4月からの案件ではRails / RSpecで開発しており、実務では初めてRSpecのmockを使ったので記事にしておきます。digメソッドを使った場合のcredentialsのmockも少しだけ詰まったので併せて。

RSpecでmockしたいケース

例えば以下のようなコントローラーがあるとします。

class BooksController < ApplicationController
  def create
    begin
      book = Book.create!(book_params)
    rescue => e
      # 何らかのエラー処理
    end

    HogeMailer.perform_later(book)
    FugaMailer.perform_later(book)

    render status: :created
  end
end

リクエストスペックでの成功パターンのテストケースとしては

  • Bookレコードが一件増えていること
  • HogeMailerが正しく動くこと
  • FugaMailerが正しく動くこと
  • 201が返却されること

ざっくりこの辺りでしょうか。

ここでMailerが動くことのテストをどう考えるのか、内部の動きまでテストするかどうか迷うかと思いますが、Mailerに限らず基本的にはモジュール単体でのテストを行い、内部の動きはそちらで担保する方が疎なテスト戦略としては良いのではないかと思っています。(もちろんチームによってテスト戦略は異なると思いますし、結合テストはある意味コスパが良いので、リクエストスペックだけでテストを行う戦略もあると思います)

ではリクエストスペックにおいてMailerの動きをどう扱うのか、ここで登場するのがテストでお馴染みのmockですね。

テストコード

まずMailer以外のテストコードです。

require "rails_helper"

RSpec.describe "Books", type: :request do
  let(:body) { JSON.parse(response.body) }
  let!(:params) { book: attributes_for(:book) }
  subject { post "api/books", params: params }

  describe "成功" do
    it "Bookのレコードが1件増えている" do
      expect { subject }.to change { Book.count }.by(1)
    end

    it "201が返却される" do
      subject

      expect(response).to have_http_status(201)
    end
  end
end

レコードが増え、201が返却されるテストはこんな感じかと思います。

そしてMailerのmockですが、まず構文は以下です。

allow(mockしたいオブジェクト).to receive(:hoge_method)

戻り値をつけたい場合

allow(mockしたいオブジェクト).to receive(:hoge_method).and_return('hogehoge')

クラスメソッドは上記でできますが、インスタンスメソッドをmockしたい場合もあるかと思います。その場合はreceive_message_chainが使用できます。

allow(mockしたいオブジェクト).to receive_message_chain(:new, :fuga_method).and_return('fugafuga')

テストコード上では以下のようにmockを作成します。

# 略

describe "成功" do
    before do
      allow(HogeMailer).to receive(:perform_later)
      allow(FugaMailer).to receive(:perform_later)
    end

# 略

end

このようにmockを作成することで、リクエストスペック上ではxxxxMailerperform_laterが呼び出されたこと、だけを検証することができます。

require "rails_helper"

RSpec.describe "Books", type: :request do
  let(:body) { JSON.parse(response.body) }
  let!(:params) { book: attributes_for(:book) }
  subject { post "api/books", params: params }

  describe "成功" do
    it "Bookのレコードが1件増えている" do
      expect { subject }.to change { Book.count }.by(1)
    end

    it "HogeMailerのperform_laterが呼ばれる" do # 追加
      subject

      expect(HogeMailer).to have_received(:perform_later).once
    end

    it "FugaMailerのperform_laterが呼ばれる" do # 追加
      subject

      expect(FugaMailer).to have_received(:perform_later).once
    end


    it "201が返却される" do
      subject

      expect(response).to have_http_status(201)
    end
  end
end

Railsで言うと、Jobもジョブスペックに任せて、リクエストスペックではmock化するのがいいと考えてます。もちろん自作モジュール等も。

余談ですが、テストにおけるmock化を考えられるようになってからは実装の設計において疎結合ということをよく考えられるようになったかもしれません。Everyday Rails外から中へ進む注釈で紹介されている「リファクタリング」はこのことかもしれないなと思ってます(ちゃんと読まなきゃ)。

credentialsのmock

最後にcredentialsのmockですが、上記Mailerと同様、credentials.ymlに記載している内容の通り書くことができます。

# hogehoge: abcd
allow(Rails.application.credentials).to receive(:hogehoge).and_return('abcd')

ネストしたcredentialsの場合も、receive_message_chainで書けると思います。 一方、digメソッドを使ったネストしたcredentialsの時に少し詰まりました。別環境では使わないのだけど、コード上どうしても読み込まれてしまうcredentialsに対して、nilを返してもらうことで読み込み時の例外発生を避けたい時に使うやつですね。

終わってみて考えればdigメソッドに対して引数を渡しているだけなので、何てことはないものでしたね…。

# hoge: 
#   fuga:
#     piyo: 1234
allow(Rails.application.credentials).to receive(:dig).with(:hoge, :fuga, :piyo).and_return('1234')

終わりに

Railsを本格的に触るのは4月からでしたが、Rspecがとても簡潔に書けるので楽しいです。感覚としてはフロントエンドのテストを書いているようです。Ruby自体も、それまでに開発していたPHPと比べてシンプルで楽しいなと思います。 また経験2言語目ということで、「これ進研ゼミで見たやつだ!」を頻繁に体感しているおかげもあるかもしれません。

本で言うとrubyに関して現在はチェリー本を読んでいますが、この後はオブジェクト指向設計実践ガイドも読んでみたいですね。

参考

使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita

Is there a way to stub Rails credential key? · Issue #2099 · rspec/rspec-rails · GitHub

Everyday Rails… Aaron Sumner 著 et al. [Leanpub PDF/iPad/Kindle]