Rspecで複数モデルの個数の増減を１つのテストで検証するには

# はじめに
Rspecを追加していて、１つのテスト項目で複数のモデルの個数が増えていること・変化しないことを検証する必要がありました

その時に便利なものを見つけたのでメモで残します

## 背景
userを消したときに紐づくSNSの中間テーブルは削除、紐づくSNSアカウントは削除しないという処理がありました

##### 具体的な例
以下のようなuserテーブルを削除したときのモデルの個数を検証するテストでした

```models/user.rb 
has_many :youtube_certs, dependent: :destroy
has_many :youtube_users, through: :youtube_certs
has_many :instagram_certs, dependent: :destroy
has_many :instagram_users, through: :instagram_certs
has_many :twitter_certs, dependent: :destroy
has_many :twitter_users, through: :twitter_certs
```

簡単にいうと
userを削除・・・・
　　　　XXX(userに紐づく各SNS)_certsテーブルは削除
　　　　XXX(userに紐づく各SNS)_usersテーブルは削除しない

## １番最初の書き方
１行ずつ記載していたが、無駄な処理が多い・・・
```spec/user_spec.rb
context "インフルエンサーを削除できる" do
  it "各SNSの_certsテーブルが削除されること" do
    expect { delete user_path(user) }.to change(YoutubeCert, :count).by(-1)
    expect { delete user_path(user) }.to change(InstagramCert, :count).by(-1)
    expect { delete user_path(user) }.to change(TwitterCert, :count).by(-1)
  end

  it "各SNSの_usersテーブルは削除されないこと" do
    expect { delete user_path(user) }.to change(YoutubeUser, :count).by(0)
    expect { delete user_path(user) }.to change(InstagramUser, :count).by(0)
    expect { delete user_path(user) }.to change(TwitterUser, :count).by(0)
  end
end
```

## マッチャ合成式を使用した書き方
重複が減って削除する処理の回数も2回に減った
```spec/user_spec.rb
context "インフルエンサーを削除できる" do
  it "各SNSの_certsテーブルが削除されること" do
    expect do
      delete user_path(user)
    end.to change(YoutubeCert, :count).by(-1).and change(InstagramCert, :count).by(-1).and change(TwitterCert, :count).by(-1)
  end

  it "各SNSの_usersテーブルは削除されないこと" do
    expect do
      delete user_path(user)
    end.to change(YoutubeUser, :count).by(0).and change(InstagramUser, :count).by(0).and change(TwitterUser, :count).by(0)
end
```

### 複数モデルの個数が減るパターンのテストは完成！！


#### しかし・・・個数が変わらないことのテストでrubocopの指摘
```rubocop
Prefer negated matchers with compound expectations over change.by(0).
```
→`change.by(0)`を使用せずに`not_to change`の使用を提案された

## not_to changeを使用した書き方
```spec/user_spec.rb
context "インフルエンサーを削除できる" do
~省略~
  it "各SNSの_usersテーブルは削除されないこと" do
    expect do
      delete user_path(user)
    end.to_not change(YoutubeUser, :count).and to_not change(InstagramUser, :count).and to_not change(TwitterUser, :count)
end
```
上記の書き方だとエラーが発生
```
NoMethodError:
  undefined method `to_not' for #......>
```
`not_to change ... and to_not change ...`のような書き方はできないみたいでした

以下がとても参考になりました！
URL：[Rspecで複数のモデルのカウントが増加しないことを検証する](https://qiita.com/togo_mentor/items/b0be5b2e22abb3997144)


#### Define negated matcherを使用するとうまくいくみたい
マッチャの否定形を***自分で***作ることができるみたいです

```ruby
# 第一引数:自分が定義したい否定系のマッチャ
# 第二引数:否定系にしたい既存のマッチャ
# 定義する場所:マッチャを使いたいテストがあるspecファイルの先頭などに記載する
RSpec::Matchers.define_negated_matcher :not_change, :change
```

## Define negated matcherを使用した書き方
```spec/user_spec.rb
context "インフルエンサーを削除できる" do
~省略~
  it "各SNSの_usersテーブルは削除されないこと" do
    # 今回追加した
    RSpec::Matchers.define_negated_matcher :not_change, :change
    expect do
      delete user_path(user)
    end.to not_change(YoutubeUser, :count).and not_change(InstagramUser, :count).and not_change(TwitterUser, :count)
end
```
無事テストが通りました

## まとめ
Define negated matcherを使用すると複数モデルの個数の増減両方一気に検証こともできました
→not_changeとchangeをandで繋いで検証できる！！！

```spec/user_spec.rb
context "インフルエンサーを削除できる" do
~省略~
  it "各SNSの_usersテーブルは削除されないこと" do
    RSpec::Matchers.define_negated_matcher :not_change, :change
    expect do
      delete user_path(user)
    end.to not_change(YoutubeUser, :count).and change(InstagramCert, :count).by(-1)
end
```

## 参考
[Rspecで複数のモデルのカウントが増加しないことを検証する](https://qiita.com/togo_mentor/items/b0be5b2e22abb3997144)
[RSpecのDefine negated matcherが地味に便利](https://qiita.com/harashoo/items/a4f1e01d300b6d329dee)