[RSpec]クラスメソッドを通過する引数の検査
こんなコードがあった、としまして、
class User < ApplicationRecord
def fetch_external_with_initial_value
# morningはusersテーブルに存在してる
greet = morning ? "Good morning" : "Hi"
# 外部のapiにリクエストしているつもり
ExternalApi.new(greet:).fetch
end
end
class ExternalApi
def initialize(greet: "good by");end
# このメソッドはfaradayなどで、外部にリクエストする想定
def fetch
"hello"
end
end
fetch_external_with_initial_valueメソッドを実行するときにExternalApiのnewの引数をrspecで確認したいというようなことがありました。
この例だと単純に下記の部分をメソッド化して、そのメソッドに対するテストを書けば良さそうではあります。
greet = morning ? "Good morning" : "Hi"
ですが、実際のコードはもう少し複雑だったり、メソッド化しすぎると処理が追いづらいなど色々な意見があって、難しかったりします。
というわけで、newの引数を直接検査しようと思ったのですが、newはクラスメソッドです。
あれ、クラスメソッドをスタブ化するのどうやるんだっけ?と毎回なります。
Stack Overflowなどでヒットするのが、class_double(Object).as_stubbed_const
の様な書き方でした。
まずは、こんなfixturesを用意します。
morning_man:
id: 1
morning: true
morning_manはmorningがtrueなので、fetch_external_with_initial_valueメソッド内のgreetは"Good morning"になるはずです。
そしてspecはこんな感じ↓
require 'rails_helper'
RSpec.describe User, type: :model do
fixtures :users
it 'newはクラスメソッドだからclass_doubleを使う?' do
class_double(ExternalApi).as_stubbed_const
# #<ClassDouble(ExternalApi) (anonymous)> received unexpected message :new with ({:greet=>"Good morning"})
users(:morning_man).fetch_external_with_initial_value
expect(ExternalApi).to have_received(:new).with(greet: "Good morning")
end
end
users(:morning_man).fetch_external_with_initial_value
の部分でエラーが発生します。
これは、ClassDoubleでnewメソッドが定義されてないという意味になると思います。
と言うわけで改良して、
it 'ExternalApiのnewとfetchを補った後で、as_stubbed_constする' do
class_double(ExternalApi, new: instance_double(ExternalApi, fetch: 'hello')).as_stubbed_const
users(:morning_man).fetch_external_with_initial_value
expect(ExternalApi).to have_received(:new).with(greet: "Good morning")
end
これならうまくいきますが、なんかコードが長いですね。
fetch_external_with_initial_valueメソッドの中で、ExternalApi.newの後で、fetchメソッドも呼んでるところが複雑にさせてます。
結局は↓でやりたいことができます。
it '依存しているクラスのnewをスタブ化して、newの引数を検査する' do
# allowを使うと特定のメソッドだけスタブ化できる。
# さらにand_call_originalを使ってnewが元々の動きをすることで、
# その後のfetchメソッドの動きにも影響を与えない。
allow(ExternalApi).to receive(:new).and_call_original
users(:morning_man).fetch_external_with_initial_value
expect(ExternalApi).to have_received(:new).with(greet: "Good morning")
end
class_doubleは使わなくていいんですよね。久しぶりに、rspec mockのREADMEを見るとわからなくなるので、まとめてみました。
今回使ったコードはこちらに置いてあります。
Discussion