Tbpgr Blog

Ruby プログラマ tbpgr(てぃーびー) のブログ

RSpec のテストダブルで呼び出しているクラスやメソッドの変更を検出する方法について

f:id:tbpg:20170324030430p:plain

RSpec のテストダブルで呼び出しているクラスやメソッドの変更を検出する方法についてまとめます。

スライド

まずは Sandi Metz 氏の自動テストに関するスライドをご覧ください。

Verifying doubles

スライドではテストダブルが実際にAPIと同期しているかチェックするためのライブラリ群が紹介されています。
RSpec3 からは該当機能が標準で取り込まれています。

relishapp.com

例えば、 RSpec 2 で double を利用していた箇所を
RSpec 3 で instance_double, class_double 等を利用するように変更すると、
テストダブルの変更を検出してくれるようになります。
これにより該当クラスやメソッドの名前が変更された場合に変更を検出できるようになります。

さらに RSpec の設定値を変えることで検証範囲を変更することができます。
以下でサンプルを紹介します。

サンプル

ケース verify_doubled_constant_names エラーを検出できるか?
double でクラスが存在しないケース
double でメソッドが存在しないケース
instance_double でクラスが存在しないケース false
instance_double でメソッドが存在しないケース false
instance_double でクラスが存在しないケース true
instance_double でメソッドが存在しないケース true

※今回はサンプルとして instance_double を使っていますが、
class_double, object_double などもあります。

double でクラスが存在しないケース

require "spec_helper"

# class Hoge
  # def hoge
    # "hoge"
  # end
# end

class HogeUser
  def initialize(hoge)
    @hogea = hoge
  end

  def use
    @hogea.hoge
  end
end

describe HogeUser do
  it do
    hoge = double("Hoge")
    expect(hoge).to receive(:hoge)

    user = HogeUser.new(hoge)
    user.use
  end
end
  • 結果

正常終了してしまいます。
モック対象の変更やタイポをテストで検出することができません。

$ rspec spec/double_not_exist_class_spec.rb
.

Finished in 0.00512 seconds (files took 0.10376 seconds to load)

double でメソッドが存在しないケース

require "spec_helper"

class Hoge
  # def hoge
    # "hoge"
  # end
end

class HogeUser
  def initialize(hoge)
    @hogea = hoge
  end

  def use
    @hogea.hoge
  end
end

describe HogeUser do
  it do
    hoge = double("Hoge")
    expect(hoge).to receive(:hoge)

    user = HogeUser.new(hoge)
    user.use
  end
end
  • 結果

正常終了してしまいます
モック対象の変更やタイポをテストで検出することができません。

$ rspec spec/double_not_exist_method_spec.rb
.

Finished in 0.00512 seconds (files took 0.10597 seconds to load)

mocks.verify_doubled_constant_names = false の場合

  • spec_helper.rb
  # 略
  config.mock_with :rspec do |mocks|
    # ※設定を省略した場合はデフォルトで false になります
    mocks.verify_doubled_constant_names = false
  end
  # 略

instance_double でクラスが存在しないケース

require "spec_helper"

# class Hoge
  # def hoge
    # "hoge"
  # end
# end

class HogeUser
  def initialize(hoge)
    @hogea = hoge
  end

  def use
    @hogea.hoge
  end
end

describe HogeUser do
  it do
    hoge = instance_double("Hoge")
    expect(hoge).to receive(:hoge)

    user = HogeUser.new(hoge)
    user.use
  end
end
  • 結果

正常終了してしまいます。
モック対象の変更やタイポをテストで検出することができません。

$ rspec spec/instance_double_not_exist_class_spec.rb
.

Finished in 0.00689 seconds (files took 0.10842 seconds to load)
1 example, 0 failures

instance_double でメソッドが存在しないケース

require "spec_helper"

class Hoge
  def hoge
    "hoge"
  end
end

class HogeUser
  def initialize(hoge)
    @hogea = hoge
  end

  def use
    @hogea.hoge
  end
end

describe HogeUser do
  it do
    hoge = double("Hoge")
    # わざと誤ったメソッド名を指定
    expect(hoge).to receive(:hogea)

    user = HogeUser.new(hoge)
    user.use
  end
end
  • 結果

メソッドが存在しないとエラーになります。
メソッド名の変更をエラーとして検出できます。

$ rspec spec/instance_double_not_exist_method_spec.rb
F

Failures:

  1) HogeUser should receive hogea(*(any args)) 1 time
     Failure/Error: @hogea.hoge
       #<Double "Hoge"> received unexpected message :hoge with (no args)
     # ./spec/instance_double_not_exist_method_spec.rb:15:in `use'
     # ./spec/instance_double_not_exist_method_spec.rb:25:in `block (2 levels) in <top (required)>'

Finished in 0.0051 seconds (files took 0.10938 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/instance_double_not_exist_method_spec.rb:20 # HogeUser should receive hogea(*(any args)) 1 time

mocks.verify_doubled_constant_names = true の場合

  • spec_helper.rb
  # 略
  config.mock_with :rspec do |mocks|
    mocks.verify_doubled_constant_names = true
  end
  # 略

instance_double でクラスが存在しないケース

ソースコードは前の例と同じなので省略

  • 結果

クラスが存在しないとエラーになります。
クラス名の変更をエラーとして検出できます。

$ rspec spec/instance_double_not_exist_class_spec.rb
F

Failures:

  1) HogeUser
     Failure/Error: hoge = instance_double("Hoge")
       "Hoge" is not a defined constant. Perhaps you misspelt it? Disable check with `verify_doubled_constant_names` configuration option.
     # ./spec/instance_double_not_exist_class_spec.rb:21:in `block (2 levels) in <top (required)>'

Finished in 0.00048 seconds (files took 0.1098 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/instance_double_not_exist_class_spec.rb:20 # HogeUser

instance_double でメソッドが存在しないケース

  • ソースコードmocks.verify_doubled_constant_names = false の例と同じなので省略
  • 実行結果は mocks.verify_doubled_constant_names = false の例と同じなので省略

Verifying doubles についてまとめ

デフォルトではメソッドのチェックはしてくれますが、クラスの存在チェックはしてくれません。
mocks.verify_doubled_constant_names = true を設定すればクラスの存在チェックもしてくれます。
なぜこの挙動がデフォルトなのか、よくわかっていません。

関連資料