【Ruby】Minitestでスタブを使う(stub/stub_any_instance)

# はじめに

スタブ(stub)とは、テスト中で呼ばれるメソッド処理の代用品のようなものです。
テストで呼ばれるメソッドの処理を指定した処理で置き換えることができます。
「外部サービスの呼び出し処理などを置き換えることによって仮状態で動かして、本当にテストしたい部分へ簡易的に辿りつきたい」といったときによくスタブを使います。

以下では例として次のような`SampleService`のテストを作成することを考えます。

```ruby
class SampleService
  def initialize; end

  def call
    is_error1 = call_api1
    is_error2 = CallApi2Service.new.call
    return 'error!' if is_error1 || is_error2

    'success!'
  end

  private

  def call_api1
    # 外部API1の呼び出し処理
  end
end
```
```ruby
class CallApi2Service
  def initialize; end

  def call
    # 外部API2の呼び出し処理
  end
end
```

「2つの外部APIを両方ともエラーなく呼び出せたときに`’success!’`が返却される」ということのテストを次のように作成したとします。

```ruby
class SampleServiceTest < MiniTest::Test
  def test_success
    sample_service = SampleService.new
    assert_equal('success!', sample_service.call)
  end
end
```

このとき外部API1と外部API2をテスト実行の度に呼び出すことになります。
また外部API1や外部API2との通信が必ずしも成功するとは限りません。
そこで2つの外部API呼び出し処理を別の処理で置き換えて、通信エラーが生じない状況をテスト実行の度に再現できるようにしていきます。

# stub

`stub`を使うと、特定のオブジェクトのメソッドを他の処理で置き換えることができます。
`SampleService`の`call_api1`メソッドを、`stub`によって「(外部API1を呼び出すことなく)falseを返却する」という処理で置き換えると次のようになります。

```ruby
class SampleServiceTest < MiniTest::Test
  def test_success
    sample_service = SampleService.new
    sample_service.stub(:call_api1, false) do
      assert_equal('success!', sample_service.call)
    end
  end
end
```

これで「外部API1をエラーなく呼び出す」という状態をテストの度に再現できるようになりました。

では外部API2の呼び出し処理も同様に置き換えようとするとどうなるでしょうか。
`stub`は特定のオブジェクトに対して用いるメソッドです。
そのためテストで外部API2の呼び出し処理を`stub`によって置き換えるには、テストを実行したときに生成される`CallApi2Service`インスタンスに対して`stub`を適用する必要がありますが、これは非常に困難です。

また仮に次のように`CallApi2Service`クラスに対して`stub`を使った場合は、`CallApi2Service`クラスのクラスメソッド`call`を置き換える扱いとなり、「`Minitest::UnexpectedError: NameError: undefined method 'call' for class 'CallApi2Service'`」のようなエラーを生じます。
```ruby
class SampleServiceTest < MiniTest::Test
  def test_success
    sample_service = SampleService.new
    sample_service.stub(:call_api1, false) do
      CallApi2Service.stub(:call, false) do
        assert_equal('success!', sample_service.call)
      end
    end
  end
end
```

こんなときに使えるのが`stub_any_instance`です。
`stub_any_instance`を用いて(「特定のオブジェクトに対して」ではなく)「特定のクラスから生成される全てのインスタンスに対して」インスタンスメソッドの処理を置き換えます。

# stub_any_instance

`stub_any_instance`を使うと、特定のクラスのインスタンスメソッドを他の処理で置き換えることができます。
特定のクラスのインスタンス全てに対してメソッド処理の置き換えが行われるため、`CallApi2Service`インスタンスのようにテストしたいメソッドの中で生成されるものに対して処理の置き換えをしたい場合に便利です。

`stub_any_instance`を使うには次のgemを追加する必要があります。

```ruby
gem 'minitest-stub_any_instance'
```

外部API2の呼び出し処理(`CallApi2Service`の`call`メソッド)を、`stub_any_instance`によって「(外部API2を呼び出すことなく)falseを返却する」という処理で置き換えると次のようになります。

```ruby
class SampleServiceTest < MiniTest::Test
  def test_success
    sample_service = SampleService.new
    sample_service.stub(:call_api1, false) do
      CallApi2Service.stub_any_instance(:call, false) do
        assert_equal('success!', sample_service.call)
      end
    end
  end
end
```

これで「2つの外部APIを両方ともエラーなく呼び出せたときに`’success!’`が返却される」ということをテストできるようになりました。

# まとめ
- スタブを使うとテストで実行される処理を別の処理に置き換えることができる
- 特定のオブジェクトのメソッドを他の処理で置き換えたいとき → `stub`
- 特定のクラスのインスタンス全てに対してインスタンスメソッドを他の処理で置き換えたいとき → `stub_any_instance`

# 参考

- [Method: Object#stub](https://www.rubydoc.info/gems/minitest/Object:stub)
- [https://github.com/codeodor/minitest-stub_any_instance](https://github.com/codeodor/minitest-stub_any_instance)
- [[Rails] RSpecのモックとスタブの使い方｜TechRacho by BPS株式会社](https://techracho.bpsinc.jp/hachi8833/2014_07_10/17971)