😸

[RSpec]クラスメソッドを通過する引数の検査

2024/12/01に公開

こんなコードがあった、としまして、

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を用意します。

users.yml
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