tl;dr

awspec のコードを見ていて, どんな風に独自のマッチャを実装しているのか, ずーっと気になっていたので, Rspec のカスタム抹茶を点てる方法について調べてみたのと, 簡単なサンプル抹茶を点ててみたメモです.

茶器

以下の茶器 (環境) を利用します.

$ ruby --version
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]

$ bundle exec rspec --version
RSpec 3.7
  - rspec-core 3.7.1
  - rspec-expectations 3.7.0
  - rspec-mocks 3.7.0
  - rspec-support 3.7.1

awspec でのカスタムマッチャ

例えば, 以下のようなマッチャが定義されています.

# https://github.com/k1LoW/awspec/blob/master/lib/awspec/matcher/have_db_parameter_group.rb
RSpec::Matchers.define :have_db_parameter_group do |name|
  match do |db_instance_identifier|
    db_instance_identifier.has_db_parameter_group?(name, @parameter_apply_status)
  end

  chain :parameter_apply_status do |parameter_apply_status|
    @parameter_apply_status = parameter_apply_status
  end
end

とある RDS DB インスタンス (my-rds) にパラメータグループ default.mysql5.6 が付与されているか, ステータス (parameter_apply_status) は pending-reboot となっていることをテストするマッチャです.

以下のように利用します.

# https://github.com/k1LoW/awspec/blob/master/spec/type/rds_spec.rb
describe rds('my-rds') do
...
  it { should have_db_parameter_group('default.mysql5.6') }
  it do
    should have_db_parameter_group('default.mysql5.6')\
      .parameter_apply_status('pending-reboot')
  end
end

抹茶を点てる

RSpec::Matchers.define による抹茶の定義

RSpec::Matchers.define を利用することで, 以下のようなマッチャを定義することが出来ます.

require 'rspec/expectations'

RSpec::Matchers.define :have_word do |expected|
  match do |actual|
    actual.include?(expected)
  end
end

RSpec.describe 'foo-bar-bar' do
  it { should have_word('foo') }
end

match ブロック内に現在の状態 (actual) とあるべき状態 (expected) を比較するロジックを記載すれば良さそうです.

これを実行すると, 以下のように出力されます.

$ bundle exec rspec sample1-1.rb

foo-bar-bar
  should have word "foo"

Finished in 0.00164 seconds (files took 0.15382 seconds to load)
1 example, 0 failures

念の為, 以下のように書いて, fail になることを確認します.

... 略 ...

RSpec.describe 'foo-bar-bar' do
  it { should have_word('baz') }
end

以下, 実行結果です.

$ bundle exec rspec sample1-1.rb

foo-bar-bar
  should have word "foo"

foo-bar-bar
  should have word "baz" (FAILED - 1)

Failures:

  1) foo-bar-bar should have word "baz"
     Failure/Error: it { should have_word('baz') }
       expected "foo-bar-bar" to have word "baz"
     # ./sample1-1.rb:14:in `block (2 levels) in <top (required)>'

Finished in 0.02444 seconds (files took 0.20172 seconds to load)
2 examples, 1 failure

LGTM.

chain を使って, マッチャを拡張する

先の例では, foo-bar-bar という文字列の中に, foo というワードが含まれていることをテストしていますが, foo というワードが 1 つ含まれていることをテストするコードを書くとすると, 以下のように書きたくなると思います. (少なくとも, 自分は書きたいと思います)

RSpec.describe 'foo-bar-bar' do
  it { should have_word('foo').count(1) }
end

これを実現する為に, chain を使って, チェーンするメソッドを定義することが出来ますので, 以下のように書いてみました.

require 'rspec/expectations'

RSpec::Matchers.define :have_word do |expected|
  match do |actual|
    if @num.nil?
      actual.include?(expected)
    else
      actual.split('-').count(expected) == @num
    end
  end

  chain :count do |num|
    @num = num
  end
end

chain ブロック内で count メソッドに渡された引数をインスタンス変数に代入しています.

chain :count do |num|
    @num = num
  end

実際にテストを走らせてみます.

$ bundle exec rspec sample1-2.rb

foo-bar-bar
  should have word "foo"

foo-bar-bar
  should have word "foo"

Finished in 0.00133 seconds (files took 0.18253 seconds to load)
2 examples, 0 failures

異常値を count メソッドに定義して, fail になることも確認しておきます.

$ bundle exec rspec sample1-2.rb

foo-bar-bar
  should have word "foo"

foo-bar-bar
  should have word "foo" (FAILED - 1)

Failures:

  1) foo-bar-bar should have word "foo"
     Failure/Error: it { is_expected.to have_word('foo').count(2) }
       expected "foo-bar-bar" to have word "foo"
     # ./sample1-2.rb:23:in `block (2 levels) in <top (required)>'

Finished in 0.03156 seconds (files took 0.26405 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./sample1-2.rb:23 # foo-bar-bar should have word "foo"

LGTM.

fail 時のメッセージを定義する

fail した際のメッセージについてもカスタマイズしたメッセージを出力することが出来ます.

require 'rspec/expectations'

RSpec::Matchers.define :have_word do |expected|
  match do |actual|
    if @num.nil?
      actual.include?(expected)
    else
      actual.split('-').count(expected) == @num
    end
  end

... 略 ...

  failure_message do |actual|
    if @num.nil?
      "#{actual} に #{expected} は含まれていない."
    else
      "#{actual} に #{expected} は #{@num} 個含まれていない."
    end
  end

end

failure_message ブロックに fail 時に出力したいメッセージを記載します.

実際に fail させてみると, 以下のように出力されます.

$ bundle exec rspec sample1-3.rb

foo-bar-bar
  should have word "foo"

foo-bar-bar
  should have word "foo" (FAILED - 1)

Failures:

  1) foo-bar-bar should have word "foo"
     Failure/Error: it { is_expected.to have_word('foo').count(2) }
       foo-bar-bar に foo は 2 個含まれていない.
     # ./sample1-3.rb:32:in `block (2 levels) in <top (required)>'

Finished in 0.04706 seconds (files took 0.26292 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./sample1-3.rb:32 # foo-bar-bar should have word "foo"

イイ感じです.

ヘルパーメソッドを利用する

以下のように書くことで, 検証ロジックを別メソッドに定義することが出来ます.

RSpec::Matchers.define :have_word do |expected|
  match do |actual|
    include_word?(actual, expected)
  end

  def include_word?(actual, expected)
    if @num.nil?
      actual.include?(expected)
    else
      actual.split('-').count(expected) == @num
    end
  end
... 略 ...
end

テストを実行してみます.

$ bundle exec rspec sample1-4.rb

foo-bar-bar
  should have word "foo"

foo-bar-bar
  should have word "foo"

Finished in 0.00129 seconds (files took 0.18528 seconds to load)
2 examples, 0 failures

LGTM.

以上

カスタム抹茶を点てるには…

  • RSpec::Matchers.define を利用する
  • match ブロック内に検証のロジックを書く
  • メソッドチェインを利用したい場合には chain ブロック内で引数をインスタンス変数に代入する
  • fail 時のメッセージは failure_message で上書きすることが出来る
  • ヘルパーメソッドを利用して, 検証のロジックを match ブロックから追い出すことも可能

という感じでしょうか.

参考

本チュートリアルを行うにあたり, 以下のサイトを参考にさせて頂いております. 有難うございました.

Relish helps your team get the most from Behaviour Driven Development. Publish, browse, search, and organize your Cucumber features on the web.

relishapp.com

カスタム:tea:の作り方を勉強したのでその備忘録。# 確認したバージョン```$ ruby --versionruby 2.1.3p242 (2014-09-19 revision 47630) [x86_64-darw...

qiita.com

元記事はこちら

Rspec カスタム抹茶 (マッチャ) の点て方チュートリアル