8
3

[Rails]stubを使うときは評価順序に気をつけた方がいいという話

Last updated at Posted at 2024-09-05

はじめに

皆さんstubは好きですか?私は正直嫌いです。しかし、外部と通信を行う機能を作成したりする時には切ってもきれない便利な機能です。今回はそんな機能を使うにあたって沼ったポイントがあったので共有していきます。

開発環境

  • Ruby 3以上
  • Ruby on Rails 6以上
  • Minitest

最初に結論

Object.newstubする際は、.newされる前に引数が評価されるので注意が必要です。

これだけではわかりづらいと思うので、コードで例を見ながら解説します。

ケーススタディ

以下のような外部とやり取りを行う、GetterServiceがあったとします。外部通信を行うのはGetterClientなので、テストを行う際はGetterClient.newstubすることになります。

class GetterService < ServiceBase
  def call
    pp '1'
    data_list = getter_client.fetch_data_list
    pp '2'
    Formatter.format(data_list)
  end

  private

  def getter_client
    pp '3'
    GetterClient.new(Authorizer.authorizer)
    pp '4'
  end
end

class Authorizer
  def authorizer
    pp '5'
  end
end

以下はそのテストコードの一例です。

class InsertServiceTest < ActiveSupport::TestCase
  test '情報を取得して整形できる' do
  client_mock = Minitest::Mock.new
  client.expect(:fetch_data_list, 'dummy_response')

  GetterClient.stub(:new, client_mock) do
    res = GetterService.new.call
    # 以下resのassert処理
    end
  end
end

ここで問題

上記のコードに出力用として「1〜5」の数値をppしていますが、テストを実行した場合、どの順番でこれらが表示されると思いますか?

  1. 「1→3→4→2」と表示される
  2. 「1→3→5→4→2」と表示される
  3. どれも表示されない。エラーになる

正解

正解は2の「1→3→5→4→2」と表示される、です!
...ゑ?どうして「5」が表示されるの?GetterClient.newされる時にstubされるから引数のApiAuthorizer.authorizerは呼ばれないはずでは?
そう思った方、私と同じです。仲良くしましょう。

解説

ここでのポイントは、「GetterClient.newされるときにstubしているからAuthorizer.authorizerは呼ばれないのでは?」という部分です。

確かにGetterClient.newされた段階でstubは正常に働いており、その後本来のGetterClientが呼ばれることはありません。そう、 .newされた段階」でstubが効いているのであって、.newされる前はstubが適応されていない のです。

具体的には、GetterClient.new(Authorizer.authorizer)では、GetterClient.newされる前にAuthorizer.authorizerが評価されています。つまり、.newの引数が評価されてから、.newが実行されるという順序になっているのです。

Rubyの評価順序について

Rubyのリファレンスマニュアルを見てみると、

まずレシーバ式を評価してレシーバとなるオブジェクトを得ます。レシーバ式が省略された場合は呼び出しを行っているブロックのself がレシーバです。
続いて引数式を左から右の順番で評価し、レシーバに対してメソッドの検索を行います。検索が失敗したら例外 NameError を発生、成功したらメソッドを実行します。

とあります。まとめると、

  1. レシーバ式(ここではGetterClient)が評価される
  2. 引数式(Authorizer.authorizer)が左から右の順で評価される
  3. レシーバに対してメソッドが実行される

コードで表すと以下のようになります。

class ExampleService
    def call
        ExampleObject.new(call_first, call_second)
    end

    private

    def call_first
        pp '1'
    end

    def call_second
        pp '2'
    end

end

class ExampleObject
    def initialize(callable1, callable2)
        @callable1 = callable1
        @callable2 = callable2
        pp '3'
    end
end

ExampleService.new.call
# "1"
# "2"                                                                         
# "3"                                                                         

このコードでは、ppが評価される順序に従ってppを配置しています。実際にコンソールで実行してみると、上記の順番で表示されるはずです。
つまり、Object.newstubしようとすると、.newの中身が評価された後にstubが効くことになります。

まとめ

Object.newstubする際も含めて、Rubyの言語仕様として、.newの引数が先に評価されることを覚えておくといつか役に立つかもしれません。今回の例のような単純な処理であれば問題ありませんが、テスト環境で動作しないような処理が含まれている場合には、原因が特定しづらくなる可能性があるので、注意が必要です(1敗)

参考

Rubyのリファレンスマニュアル

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3