11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2022-06-30

はじめに

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

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

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
class CallApi2Service
  def initialize; end

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

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

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

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'」のようなエラーを生じます。

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を追加する必要があります。

gem 'minitest-stub_any_instance'

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

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

参考

11
5
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
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?