Rubocop の RSpec/MessageSpies に対応する際は処理順の書き換えが必要
Leaner 開発チームの黒曜(@kokuyouwind)です。
AWS Startup Community Conference 2022 に CfP を出したので通ってほしい今日このごろです。
さて、今回は Rubocop 警告の修正で少し手間取った話を書きます。
RSpec/MessageSpies に引っかかったコード
あるクラスで「Rails.logger
に特定文字列を出力していることを検証したい」というケースがあり、この spec を以下のように書いていました。
# ログを出力するクラス
class Hoge
def log
Rails.logger.info('test message')
end
end
# spec
RSpec.describe Hoge do
describe 'log' do
let(:logger) { instance_double(ActiveSupport::Logger) }
before { allow(Rails).to receive(:logger).and_return(logger) }
it 'ログ出力される' do
expect(logger).to receive(:info).with('test message')
described_class.new.log
end
end
end
このファイルを標準設定の Rubocop にかけると、以下のように RSpec/MessageSpies ルールに引っかかります。
Offenses:
spec/hoge_spec.rb:18:25: C: RSpec/MessageSpies: Prefer have_received for setting message expectations. Setup logger as a spy using allow or instance_spy.
expect(logger).to receive(:info).with('test message')
^^^^^^^
1 file inspected, 1 offense detected
「メッセージ受信の検査をする場合は( receive
ではなく) have_received
を使え」と言っていますね。
RSpec/MessageSpies への対応
ここで当初、あまり深く考えずに「なるほど matcher を書き換えればいいんだな」と解釈して以下のように書き換えました。
it 'ログ出力される' do
- expect(logger).to receive(:info).with('test message')
+ expect(logger).to have_received(:info).with('test message')
described_class.new.log
end
…おわかりいただけたでしょうか。この書き換えではテストが落ちるようになってしまいます。
Failures:
1) Hoge log ログ出力される
Failure/Error: expect(logger).to have_received(:info).with('test message')
#<InstanceDouble(ActiveSupport::Logger) (anonymous)> expected to have received info, but that object is not a spy or method has not been stubbed.
receive
matcher は「メッセージを(これから)受信するか」を検査するのに対し、 have_received
は名前の通り「メッセージを(これまでに)受信したか」を検査します。
このため、 receive
は「テスト対象処理の前」に書く必要があるのに対し、 have_received
は「テスト対象処理の後」に書く必要があります。
つまり、以下のように書き換えるのが正解です。
it 'ログ出力される' do
- expect(logger).to receive(:info).with('test message')
described_class.new.log
+ expect(logger).to have_received(:info).with('test message')
end
double へのメッセージ受信定義
さきほど「以下のように書き換えるのが正解です」と書きましたが、実際にはこの書き換えだけだと別の原因でテストが失敗します。
Failures:
1) Hoge log ログ出力される
Failure/Error: Rails.logger.info('test message')
#<InstanceDouble(ActiveSupport::Logger) (anonymous)> received unexpected message :info with ("test message")
double
や instance_double
では事前に受信可能なメッセージを指定しておかないと、上述のように received unexpected message
が発生してしまいます。
元々の処理順では expect(...).to receive
の形で info
を受信可能にしていましたが、それが消えてしまったわけですね。
なので以下のように allow(...).to receive
の形で受信可能なメッセージを指定します。
it 'ログ出力される' do
+ allow(logger).to receive(:info)
described_class.new.log
expect(logger).to have_received(:info).with('test message')
end
これでテストが通るようになります。
spy への書き換え
今回のように double に対するメソッド呼び出しの戻り値に興味がない場合は、代わりに spy を使うと受信可能なメッセージの指定が不要になります。
spy を利用する場合は以下のように書き換えます。
- let(:logger) { instance_double(ActiveSupport::Logger) }
+ let(:logger) { instance_spy(ActiveSupport::Logger) }
before { allow(Rails).to receive(:logger).and_return(logger) }
it 'ログ出力される' do
described_class.new.log
expect(logger).to have_received(:info).with('test message')
end
spy では mock と違い、事前に指定されていないメッセージの受信時には nil を返すので、事前の allow(...).to receive
が消えてシンプルになりました。
一方で意図しないメッセージを受信してもエラーになってくれないため、厳密な呼び出し検査を行う際には mock のほうが適していそうです。
double, spy と instance_double, instance_spy の違い
コード例では double
, spy
ではなく instance_double
, instance_spy
を使っています。
double
や spy
が任意のメッセージを受信可能なのに対し、 instance_double
や instance_spy
は指定したクラスにインスタンスメソッド定義されたメッセージのみ受信できるという違いがあります。
なお今回のコード例で double
や spy
を利用すると、以下のように RSpec/VerifiedDoubles ルールに怒られます。
Offenses:
spec/hoge_spec.rb:13:20: C: RSpec/VerifiedDoubles: Prefer using verifying doubles over normal doubles.
let(:logger) { spy(ActiveSupport::Logger) }
大抵のケースでは特定クラスのインスタンスであることを想定してコードを書くはずなので、 instance_double
や instance_spy
を利用するのが良さそうですね。
まとめ
RSpec/MessageSpies
ルールの修正方法と、それに伴う double
と spy
の違いをまとめました。
receive
と have_received
というメソッド名を見れば明らかなんですが、よく見ず手癖で修正しようとすると引っかかりやすいので気をつける必要がありそうです。
またこの警告をそもそも受けないよう、普段から「メッセージ受信に対するスタブ」と「メッセージ受信の検証」を分けて考えた上で実装するのが良さそうですね。
宣伝
Leaner Technologies では double と stub を使いこなしたいエンジニアを募集しています!
Discussion
こんにちは。わかりやすく勉強になりました。
.rubocop.ymlファイルを以下のように変更すれば、receiveを使うことができるようになるみたいですね。
コメントありがとうございます!
そうですね、おっしゃるとおりRubocop/Rspecの設定によってどちらの記述を標準とするかは変わってきます。
今回はデフォルト設定が
have_received
になっており、特段のこだわりがなかったため推奨値に従っておこうと書き換えたものになります。ご指摘ありがとうございました!