【RSpec】Shoulda Matchersの書き方について

## はじめに
### [shoulda-matchers](https://github.com/thoughtbot/shoulda-matchers)とは
Railsのテストコードをより簡潔かつ読みやすくするためのRSpec用の拡張ライブラリです。

#### Q. 何が良いのか？
A. [<font color="#0000ff">特に複雑なテストケースを1行で記述できることが多く、テストコードの行数の大幅な削減が期待できます！</font>](https://matchers.shoulda.io/docs/v6.3.0/)

#### 設定方法
以下のようにGemfileのテスト環境に```shoulda-matchers```を追加します。
（詳しくは[公式ドキュメント](https://github.com/thoughtbot/shoulda-matchers?tab=readme-ov-file#rspec)にてご確認ください🙇‍♂️）
```.Gemfile
group :test do
  gem 'shoulda-matchers', '~> 6.0'
end
```

## 今回の実装例
### 開発環境
|言語（FW）など  |バージョン  |最新ver（2024/8/11時点）  |
|---|---|---|
|Ruby  |3.0.2  |3.2.5  |
|Rails  |6.1.4  |7.1.3.4  |
|Docker-Compose  |2.27.0  |2.29.1  |
|mysql  |8.0.25  |8.0.38  |
#### 補足
フロント側はVueを使用していますが、説明の関係上で省略しています🙇‍♂️

### バリデーションの仕様
1. メールアドレスが**未入力**だった時にバリデーションエラーを発生させる
2. **ユニーク（一意）な値**でなければ、バリデーションエラーを発生させる
3. メールアドレスが**30文字を超えた場合**にバリデーションエラーを発生させる


### バリデーション
```user.rb
class User < ApplicationRecord

  validates :email, presence: true, 
            uniqueness: { case_insensitive: false, message: :taken }, 
            length: { maximum: 30 }
end
```

#### 補足
##### ```case_insensitive```  
> [一意性制約で大文字小文字を区別するか、またはデータベースのデフォルトの照合順序（collation）を尊重すべきかどうかを定義できます。このオプションはデフォルトで、データベースのデフォルト照合順序を尊重します。](https://railsguides.jp/active_record_validations.html#uniqueness)

```case_insensitive: false```の場合は大文字小文字を区別しないので、例えば```test@example.com```と```Test@example.com```のような値は異なるメールアドレスとみなされ、両方登録できる。

### エラーメッセージ
```ja.yml
  ja:
    errors:
      format: "%{attribute}%{message}"
      messages:
        taken: "はすでに存在します"

```
[ja.yml](https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml)より抜粋

### コントローラー
```users_controller.rb
class UsersController < ApplicationController

  def create
    user = User.new(user_params)
    if user.save
      render json: user, status: :created
    else
      render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.require(:user).permit(:email)
  end
end
```
#### 補足
##### errors.full_messages
[すべてのエラーメッセージを配列で取得します](https://railsdoc.com/page/errors_full_messages)

今回は個人的にフロント側はVueで実装していたため、エラーメッセージをJSONで返しています。
例えばユニーク制約のバリデーションが発生した場合に、設定した```uniqueness: { case_insensitive: false, message: :taken }```のtakenのエラーメッセージが```user.errors.full_messages```に格納されます。

### RSpec
```user_spec.rb
RSpec.describe User, type: :model do
  describe 'email' do
    subject { create(:user) }

    it { is_expected.to validate_presence_of(:email) }
    it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
    it { is_expected.to validate_length_of(:email).is_at_most(50) }
  end
end
```
#### 補足
- ```subject```:テスト対象のオブジェクト(user)を定義します。subjectで定義することによって、オブジェクト名を繰り返し記述する必要がなくなります
- ```it```: RSpecのテストケースを定義するキーワードです
- ```is_expected.to```: テストの期待値を記述する際に使用します。こちらは```expect```よりも簡潔に記述するために```subject```と一緒に使用されることが多いようです
```is_expected.to```と```expect```の違いについてはこちら➡︎[RSpec で subject & expect する時の注意点](https://qiita.com/mm36/items/453d71d48dd8f462344c)
- ```validate_uniqueness_of(:email)```: モデルのemail属性が一意であることを検証するマッチャーです
- ```case_insensitive```: 大文字小文字を区別せずに一意性を検証するオプションです

### shoulda-matchersを使わない場合
```user_spec.rb
RSpec.describe User, type: :model do
  describe "email" do
    it "validates presence" do
      user = User.new(email: "")
      user.valid?
      expect(user.errors[:email]).to include("メールアドレスが未入力です")
    end

    it "validates uniqueness (case-insensitive)" do
      user1 = User.create(email: "test@example.com")
      user2 = User.new(email: "TeSt@ExAmPlE.CoM")
      user2.valid?
      expect(user2.errors[:email]).to include("既に登録されているメールアドレスです")
    end

    it "validates length (maximum 50 characters)" do
      user = User.new(email: "a" * 51)
      user.valid?
      expect(user.errors[:email]).to include("50文字以内で入力してください")
    end
  end
end
```
差は一目瞭然ですね...
Shoulda Matchersを使うことで、テストコードの重複を減らし、保守性を高めることができます。

### テスト結果
```shell
% docker compose exec api rspec
.....................................................................
.....................................................................
.....................................................................
.....................................................................
..........................

Finished in 13.29 seconds (files took 7.46 seconds to load)
302 examples, 0 failures
```

## まとめ
Shoulda Matchersは、RSpecのテストコードをより簡潔で読みやすくするためのライブラリです。
主にモデルのバリデーションテストなどで、[マッチャー](https://matchers.shoulda.io/docs/v5.3.0/#matchers)を使用することによってワンライナー（1行）でテストコードを簡潔に記述できます。

## 参考記事
- [shoulda-matchers(Github)](https://github.com/thoughtbot/shoulda-matchers)
- [Matchers](https://matchers.shoulda.io/docs/v5.3.0/#matchers)
- [Class: Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher](https://www.rubydoc.info/gems/shoulda-matchers/Shoulda/Matchers/ActiveRecord/ValidateUniquenessOfMatcher#case_insensitive-instance_method)
- [RSpec で subject & expect する時の注意点](https://qiita.com/mm36/items/453d71d48dd8f462344c)