31
9

More than 3 years have passed since last update.

RSpecのDefine negated matcherが地味に便利

Posted at

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/supportmatchers.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が便利だと思った話でした。🍵

31
9
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
31
9