RSpec:モデルの単体テストの記述例

※2022年から技術系の記事は[個人ブログ](https://yamaday0u.net/category/programmer/)に投稿しております。ぜひこちらもご覧ください→[yamaday0u Blog](https://yamaday0u.net/category/programmer/)

今回は前回の記事「[RSpecの導入&関係の深いgemについて](https://qiita.com/yamaday0u/items/c3bf6036e11298771a99)」の続きで、モデルの単体テストを行います。

前回の記事の各gemのインストール、初期設定が完了していることを前提としています。

## モデルのテストファイルを作成
ターミナルで`rails g rspec model モデル名`コマンドを入力してテストファイルを生成します。

```zsh:ターミナル
$ rails g rspec model モデル名
#以下のように表示されればテストファイルの生成が成功しています。
Running via Spring preloader in process 1234
      create  spec/models/モデル名_spec.rb
      invoke  factory_bot
      create    spec/factories/モデル名の複数形.rb
```

たとえばuserモデルのテストファイルを生成するとこうなります。

```zsh:ターミナル
$ rails g rspec:model user
Running via Spring preloader in process 1234
      create  spec/models/user_spec.rb
      invoke  factory_bot
      create    spec/factories/users.rb #Factory Botをインストールしておくと自動生成される
```
ちなみに事前にFactory Botもインストールしておくと、`spec/factories/users.rb`は自動生成されます。

生成した`user_spec.rb`にはこのような記述がされています。

```spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end
```
`do〜end`の間にテストコードを記述していきます。

また1行目の`require 'rails_helper`は、[前回の記事](https://qiita.com/yamaday0u/items/c3bf6036e11298771a99)で行った`rails g rspec:install`コマンドで生成した`rails_helper.rb`を読み込んでいます。

## FactoryBotの設定
まずは、FactoryBotでUserモデルのテストで使用するインスタンスを設定してまとめます。

```spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { Faker::Name }
    email { Faker::Internet.free_email }
    password = Faker::Internet.password(min_length: 6)
    password { password }
    password_confirmation { password }
  end
end
```
passwordカラムとpassword_confirmationカラムは同じ値を入れるため、変数`password`に値を代入して、各カラムに変数`password`を入れます。

`Faker::`の後ろに続く`Name`や`Internet.free_email`でどんな値をランダムに生成するかを指定しています。前者なら名前、後者ならメールアドレスですね。

Fakerでは色んなタイプの値を生成してくれるので、[Fakerの公式GitHub](https://github.com/faker-ruby/faker)で見てみてください。

## テストコードの設定
Userモデル(usersテーブル）にnameカラム、emailカラム、passwordカラムがあると想定して、
たとえば以下のようにユーザー新規登録機能のテストコードを記述します。

```spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  before do
    # 各テストコードが実行される前にFactoryBotで生成したインスタンスを@userに代入
    @user = FactoryBot.build(:user)
  end

  # describeでテスト対象をグループ分け
  describe '新規登録' do
    # contextでどのような状況なのかをグループ分け
    context '新規登録できるとき' do
      # itで何をテストしているのかをグループ分け
      it '全ての項目が入力されていれば登録できる' do
        expect(@user).to be_valid
      end
    end

    context '新規登録できないとき' do
      it 'nameが空では登録できない' do
        @user.name = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Name can't be blank")
      end
      it 'emailが空では登録できない' do
        @user.email = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Email can't be blank")
      end
      it '重複したemailが存在する場合登録できない' do
        @user.save
        another_user = FactoryBot.build(:user)
        another_user.email = @user.email
        another_user.valid?
        expect(another_user.errors.full_messages).to include('Email has already been taken')
      end
      it 'emailに@が含まれていないと登録できない' do
        @user.email.slice!('@')
        @user.valid?
        expect(@user.errors.full_messages).to include('Email is invalid')
      end
      it 'passwordが空では登録できない' do
        @user.password = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Password can't be blank")
      end
      it 'passwordが5文字以下では登録できない' do
        @user.password = '123ab'
        @user.valid?
        expect(@user.errors.full_messages).to include('Password is too short (minimum is 6 characters)')
      end
      it 'passwordが存在してもpassword_confirmationが空では登録できない' do
        @user.password_confirmation = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Password confirmation doesn't match Password")
      end
    end
  end
end

```

### before do ~ end
テストコードの最初の`before do ~ end`は、describe以下の各テストコードが実行される前に処理される共通の内容を記述します。
今回の場合は登録するユーザー情報を変数`@user`に代入する処理を記述しています。
これで各テストコードでユーザー情報が入った変数`@user`を使えるようになりました。

### describeメソッド、contextメソッド、itメソッド
テストコードのグループ分けに使用するメソッドで、

+ `describe`メソッドはどのような機能なのか（テスト対象）
+ `context`メソッドはどのような状況なのか
+ `it`メソッドは何をテストしているのか

をそれぞれグループ分けしています。

### expectation(エクスペクテーション)
一番最初の「`describe`新規登録、`context`新規登録できるとき、`it`全ての項目が入力されていれば登録できる」の中にあるような以下のような記述の部分を`expectation`といい、テストで得られた結果が期待通りなのかを確認する構文です。

```ruby
expect(@user).to be_valid
```
上記の記述のうち、`be_valid`の部分を`matcher（マッチャ）`といいます。

### matcher
「expectの引数(カッコの中の記述)」と「想定した挙動」が一致しているか判断します。
expectの引数にはテストで得られた実際の挙動を指定し、matcherにはどのような挙動を想定しているかを記述します

**単体テストでよく使うmatcherの例**

|matcher|想定する挙動|記述例|
|:--|:--|:--|
|include|Xの中にYという文字列が含まれているか|expect(X).to include(Y)|
|be_valid|expectのインスタンスが正しく保存される|expect(X).to be_valid|

## テストコードの実行
少し補足説明がなりましたが、上記のテストコードの実行をします。
ターミナルで以下のコマンドを入力します。

```zsh:ターミナル
$ bundle exec rspec spec/models/user_spec.rb

# 実行結果
User
  新規登録
    新規登録できるとき
      全ての項目が入力されていれば登録できる
      passwordが6文字以上の半角英数字であれば登録できる
      identityが空で登録できる
    新規登録できないとき
      nameが空では登録できない
      emailが空では登録できない
      重複したemailが存在する場合登録できない
      emailに@が含まれていないと登録できない
      passwordが空では登録できない
      passwordが5文字以下では登録できない
      passwordが存在してもpassword_confirmationが空では登録できない

Finished in 0.07492 seconds (files took 1.3 seconds to load)
10 examples, 0 failures
```

上記ではエラーなくテストが完了しましたが、expectationに記述されているmatcherのエラーメッセージなどに記述ミスがある場合は修正する必要があります。

テストコードの修正には`pry-rails`というデバッグ用のgemを使用するのですが、それは別の記事で紹介したいと思います。

## RSpecのシリーズ記事
第1回:[RSpecの導入&関係の深いgemについて](https://qiita.com/yamaday0u/items/c3bf6036e11298771a99)
第2回:[RSpec:モデルの単体テストの記述例](https://qiita.com/yamaday0u/items/7ad2c714224a651b6189) ※この記事
## 参考資料
[RSpec: Behaviour Driven Development for Ruby](https://rspec.info/)
Qiita:
[使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」](https://qiita.com/jnchito/items/42193d066bd61c740612)