RSpecのDefine negated matcherが地味に便利でした。
特にマッチャ合成式を書くときに便利です。
マッチャ合成式(Compound Matcher Expressions)とは
andやorを使って、マッチャをつなげて書くことができます。
https://rspec.info/blog/2014/01/new-in-rspec-3-composable-matchers/
例
適当ですが、Shopというモデルのインスタンスメソッド receive_order
を実行した結果、CustomerモデルとOrderモデルのレコードが作成されることを確かめるテストがあるとします。
it 'customerとorderを作る' do
expect { shop.receive_order }.to change(Customer, :count).by(1)
expect { shop.receive_order }.to change(Order, :count).by(1)
end
上記は、マッチャ合成式を使うと以下のように書くことができます。
無駄な繰り返しが無くなって、コード量が減ります。 👏
it 'customerとorderを作る' do
expect { shop.receive_order }.to change(Customer, :count).by(1)
.and change(Order, :count).by(1)
end
Define negated matcherとは
マッチャの否定形を作ることができます。
Define negated matcherの何が便利か
使用例
先ほどのreceive_order
のテストで、Productモデルのレコードは作られないこともテストしたいとします。
このように書くと、テストは失敗します。
it 'customerとorderを作るがproductは作らない' do
expect { shop.receive_order }.not_to change(Product, :count)
.and change(Customer, :count).by(1)
.and change(Order, :count).by(1)
end
# NotImplementedError:
# `expect(...).not_to matcher.and matcher` is not supported, since it creates a bit of an ambiguity. Instead, define negated versions of whatever matchers you wish to negate with `RSpec::Matchers.define_negated_matcher` and use `expect(...).to matcher.and matcher`.
マッチャ合成式を使っている時にnotは使えないみたいです…
テストを分割すればエラーになりませんが、できれば1つにまとめて、すっきりとさせたい気持ちがあります。
it 'customerとorderを作る' do
expect { shop.receive_order }.to change(Customer, :count).by(1)
.and change(Order, :count).by(1)
end
it 'productは作らない' do
expect { shop.receive_order }.not_to change(Product, :count)
end
ここでDefine negated matcherを使います。
下記のようにchangeの否定系のマッチャを定義します。
# 第一引数に定義する否定系のマッチャ
# 第二引数に否定系にしたい既存のマッチャ
RSpec::Matchers.define_negated_matcher :not_change, :change
定義する場所は、マッチャを使いたいテストがあるspecファイルの先頭やspec/support
にmatchers.rb
など好きなファイル名で作成すると良いと思います。
上で定義したchangeの否定系のマッチャを使えば、先ほどのテストは以下のように書くことができます。
it 'customerとorderを作るがproductは作らない' do
expect { shop.receive_order }.to not_change(Product, :count)
.and change(Customer, :count).by(1)
.and change(Order, :count).by(1)
end
# もちろん and の後にも書ける
it 'customerとorderを作るがproductは作らない' do
expect { shop.receive_order }.to change(Customer, :count).by(1)
.and change(Order, :count).by(1)
.and not_change(Product, :count)
end
以上、Define negated matcherが便利だと思った話でした。🍵