[Rails]モデルの単体テスト実装方法

## 前提条件
今回は新規ユーザー登録に関するモデルの単体テストを実装していきながら、方法をアウトプットします。ユーザー登録の仕様書は以下のとおり。

```
ニックネームが必須       
メールアドレスは一意である
メールアドレスは@とドメインを含む必要がある
パスワードが必須
パスワードは7文字以上
パスワードは確認用を含めて2回入力する
ユーザー本名が、名字と名前でそれぞれ必須
ユーザー本名は全角で入力させる
ユーザー本名のフリガナが、名字と名前でそれぞれ必須
ユーザー本名のフリガナは全角で入力させる
生年月日が必須
```

マイグレーションファイルは以下のとおり。

```20200823050614_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string  :nickname,              null: false
      t.string  :email,                 null: false, default: ""
      t.string  :encrypted_password,    null: false, default: ""
      t.string  :family_name,           null: false
      t.string  :family_name_kana,      null: false
      t.string  :first_name,            null: false
      t.string  :first_name_kana,       null: false
      t.date    :birth_day,             null: false

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at
      t.timestamps null: false
    end

    add_index :users, :email,                 unique: true
    add_index :users, :reset_password_token,  unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end
```
 
モデルは以下のとおり。

```user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

    # バリデーションの設定(空の文字列を保存させない為と一意性制約)
    validates :nickname,               presence: true
    validates :encrypted_password,     presence: true
    validates :family_name,            presence: true
    validates :family_name_kana,       presence: true
    validates :first_name,             presence: true
    validates :first_name_kana,        presence: true
    validates :birth_day,              presence: true
    
    # アソシエーション
    has_many :cards,          dependent: :destroy
    has_many :shipping_infos, dependent: :destroy
    has_many :comments,       dependent: :destroy
    has_many :items
end
```

## 実装手順
**① RSpecの準備**
**② RSpecの設定**
**③ factory_botを導入**
**④ factory_botの実装**
**⑤ 正規表現の記述**
**⑥ テストコードの実装**
**⑦ テスコードの実行**

## RSpecの準備
まずGemをインストールします。

```Gemfile.rb
group :development, :test do
  
  # 省略

  gem 'rspec-rails'
end

group :development do
  gem 'web-console'
end

# web_console は既に記述がある場合は、記述場所を移動するだけでOK。追記してしまうと重複してしまう可能性があるため注意してください。
```

Gemfileを編集したら、忘れずにbundle installを実行。

## RSpecの設定
続いてRSpecの基本設定を行う。
まずはRSpec用の設定ファイルを作成する。

```ターミナル.
$ rails g rspec:install

# 以下のようにファイルが作成されれば成功 ▼
 create  .rspec
 create  spec
 create  spec/spec_helper.rb
 create  spec/rails_helper.rb
```

次に、`.rspec`に以下を追加。

```.rspec.
--format documentation
```

続いて、RSpec用ディレクトリを設定します。

RSpecによるテストコードが書かれたファイルのことを、specファイルと呼び、全てのspecファイルは、先程の`rails g rspec:install`コマンドで生成されたspecディレクトリの中に格納するので、ここで作成しておきましょう。

モデルに関するテスト用ファイルであればspec/models/以下に、コントローラーに関するテスト用ファイルであればspec/controllers/以下に格納されます。appディレクトリ以下にあるテストの対象となるコードの在り処は全てこのディレクトリです。

specファイルは対応するクラスを持つファイル名_spec.rbという名前になります。今回はまず「user.rb」に関するspecファイルを作成するので、その場合の名前は`「user_spec.rb」`になります。

ディレクトリの作成が完了したら、この時点で一度**RSpec**が正常に利用できるかの確認を行いましょう。

```ターミナル.
$ bundle exec rspec

# 以下のような結果になれば正常に動作しているのでOK
No examples found.

Finished in 0.00031 seconds (files took 0.19956 seconds to load)
0 examples, 0 failures
```

## factory_botを導入
続いて**factory_bot**を導入します。
Gemをインストール。

```Gemfile.rb
group :development, :test do
  
  # 省略

  gem 'rspec-rails'
  gem 'factory_bot_rails'
end
```

その後、bundle installを実行。

## factory_botの実装

次に、specディレクトリ直下に`factories`というディレクトリを作成し,その中に、作成したインスタンスの複数形のファイル名でRubyのファイルを作成します。今回の場合は、`users.rb`になります。

`users.rb`を以下のように編集。

```users.rb
FactoryBot.define do

  factory :user do
    nickname              {"tarou"}
    email                 {"sample@gmail.com"}
    password              {"1234567"}
    encrypted_password    {"1234567"}
    family_name           {"山田"}
    family_name_kana      {"ヤマダ"}
    first_name            {"太郎"}
    first_name_kana       {"タロウ"}
    birth_day             {"2000-01-01"}
  end
end
```

この記述は、仕様書に沿った内容をダミーデーターとして作成しており、こうすると、specファイルの中で特定のメソッド(buildやcreate)により簡単にインスタンスを生成したり、DBに保存したりできるようになります。

## 正規表現記述
続いて、仕様書に沿って、バリデーションへ正規表現を設定していきます。
必要な正規表現は以下のとおり。

```
パスワードは7文字以上
ユーザー本名は全角で入力させる
ユーザー本名のフリガナは全角で入力させる
```

これらを踏まえてモデルへ記述すると以下のとおり。

```user.rb
class User < ApplicationRecord
    # 省略

    validates :nickname,               presence: true
    validates :encrypted_password,     presence: true, length: { minimum: 7 } # ここが文字数の正規表現
    validates :family_name,            presence: true, format: {with: /\A[ぁ-んァ-ン一-龥]/ } # ここがユーザー本名全角の正規表現
    validates :family_name_kana,       presence: true, format: {with: /\A[ァ-ヶー－]+\z/ } # ここがフリガナ全角の正規表現
    validates :first_name,             presence: true, format: {with: /\A[ぁ-んァ-ン一-龥]/ } # ここがユーザー本名全角の正規表現
    validates :first_name_kana,        presence: true, format: {with: /\A[ァ-ヶー－]+\z/ } # ここがフリガナ全角の正規表現

    # 省略
end
```

Rubyの正規表現は下記サイトを参考にしてください。
https://gist.github.com/nashirox/38323d5b51063ede1d41

## テストコードの実装
それではいよいよテストコードの実装です！
テスコードを記述するときに心掛ける点は以下のとおり。

```
①各exampleで期待する値は1つ
②期待する結果をはっきりわかりやすく記述
③起きて欲しいことと起きてほしくないこと両方をテストする
④可読性を考えつつ、適度にDRYにする
```

user_spec.rbに以下のコードを記述。

```user_spec.rb
require 'rails_helper'

describe User do
  describe '#create' do

  end
end
```

今回はUserモデルの単体テストなので`describe User do`となり、新規ユーザーを作成するので`describe '#create' do`としており、2行目のdescribの中にテストコードを記述します。

まず、**全ての項目の入力が存在すれば登録できること**をテストします。
コードとその解説は以下の通り。

```user_spec.rb
require 'rails_helper'

describe User do
  describe '#create' do
    # 入力されている場合のテスト ▼

    it "全ての項目の入力が存在すれば登録できること" do # テストしたいことの内容
      user = build(:user)  # 変数userにbuildメソッドを使用して、factory_botのダミーデータを代入
      expect(user).to be_valid # 変数userの情報で登録がされるかどうかのテストを実行
    end
  end
end
```

続いてテストすることは以下の通り。

```
nicknameがない場合は登録できないこと
emailがない場合は登録できないこと
passwordがない場合は登録できないこと
encrypted_passwordがない場合は登録できないこと
family_nameがない場合は登録できないこと
family_name_kanaがない場合は登録できないこと
first_nameがない場合は登録できないこと
first_name_kanaがない場合は登録できないこと
birth_dayがない場合は登録できないこと
```

これらの条件は仕様書の必須項目(null:false, presence: true)をテストする為のものです。
それでは記述していきましょう。解説は**nicknameがない場合は登録できないこと**のテスト箇所で行います。

```user.spec.rb
require 'rails_helper'

describe User do
  describe '#create' do

      # 入力されている場合のテスト ▼

      it "全ての項目の入力が存在すれば登録できること" do
        user = build(:user)
        expect(user).to be_valid
      end

      # nul:false, presence: true のテスト ▼

      it "nicknameがない場合は登録できないこと" do # テストしたいことの内容
        user = build(:user, nickname: nil) # 変数userにbuildメソッドを使用して、factory_botのダミーデータを代入(今回の場合は意図的にnicknameの値をからに設定)
        user.valid? #バリデーションメソッドを使用して「バリデーションにより保存ができない状態であるか」をテスト
        expect(user.errors[:nickname]).to include("を入力してください") # errorsメソッドを使用して、「バリデーションにより保存ができない状態である場合なぜできないのか」を確認し、.to include("を入力してください")でエラー文を記述(今回のRailsを日本語化しているのでエラー文も日本語)
      end

      it "emailがない場合は登録できないこと" do
        user = build(:user, email: nil)
        user.valid?
        expect(user.errors[:email]).to include("を入力してください")
      end

      it "passwordがない場合は登録できないこと" do
        user = build(:user, password: nil)
        user.valid?
        expect(user.errors[:password]).to include("を入力してください")
      end

      it "encrypted_passwordがない場合は登録できないこと" do
        user = build(:user, encrypted_password: nil)
        user.valid?
        expect(user.errors[:encrypted_password]).to include("を入力してください")
      end

      it "family_nameがない場合は登録できないこと" do
        user = build(:user, family_name: nil)
        user.valid?
        expect(user.errors[:family_name]).to include("を入力してください")
      end

      it "family_name_kanaがない場合は登録できないこと" do
        user = build(:user, family_name_kana: nil)
        user.valid?
        expect(user.errors[:family_name_kana]).to include("を入力してください")
      end

      it "first_nameがない場合は登録できないこと" do
        user = build(:user, first_name: nil)
        user.valid?
        expect(user.errors[:first_name]).to include("を入力してください")
      end

      it "first_name_kanaがない場合は登録できないこと" do
        user = build(:user, first_name_kana: nil)
        user.valid?
        expect(user.errors[:first_name_kana]).to include("を入力してください")
      end

      it "birth_dayがない場合は登録できないこと" do
        user = build(:user, birth_day: nil)
        user.valid?
        expect(user.errors[:birth_day]).to include("を入力してください")
      end
  end
end
```

続いて、emailの一意性制約のテストを行います。

```user.spec.rb
require 'rails_helper'

describe User do
  describe '#create' do

      # 省略

      # email 一意性制約のテスト ▼

      it "重複したemailが存在する場合登録できないこと" do
        user = create(:user) # createメソッドを使用して変数userとデータベースにfactory_botのダミーデータを保存
        another_user = build(:user, email: user.email) # 2人目のanother_userを変数として作成し、buildメソッドを使用して、意図的にemailの内容を重複させる
        another_user.valid? # another_userの「バリデーションにより保存ができない状態であるか」をテスト
        expect(another_user.errors[:email]).to include("はすでに存在します") # errorsメソッドを使用して、emailの「バリデーションにより保存ができない状態である場合なぜできないのか」を確認し、その原因のエラー文を記述
      end
  end
end
```

続いて、確認用パスワードがなければ登録できないテストを実行します。

```user.spec.rb
require 'rails_helper'

describe User do
  describe '#create' do

      # 省略

      # 確認用パスワードが必要であるテスト ▼
      
      it "passwordが存在してもencrypted_passwordがない場合は登録できないこと" do
        user = build(:user, encrypted_password: "") # 意図的に確認用パスワードに値を空にする
        user.valid?
        expect(user.errors[:encrypted_password]).to include("を入力してください", "は7文字以上で入力してください")
      end
  end
end
```

ここまでで、以下のテストは完了しました。

```
# 完了したテスト▼

ニックネームが必須 # 完了
メールアドレスは一意である # 完了
メールアドレスは@とドメインを含む必要がある # 完了
パスワードが必須 # 完了
パスワードは確認用を含めて2回入力する # 完了
ユーザー本名が、名字と名前でそれぞれ必須 # 完了
ユーザー本名のフリガナが、名字と名前でそれぞれ必須 # 完了
生年月日が必須 # 完了

# これから実装するテスト▼

パスワードは7文字以上
ユーザー本名は全角で入力させる
ユーザー本名のフリガナは全角で入力させる
```

それでは、ここからは先ほど記述した正規表現のテストを行います。
まずパスワードは7文字以上のテストから。

**起きて欲しいことと起きてほしくないこと**の両方をテストします。

```user.spec.rb
require 'rails_helper'

describe User do
  describe '#create' do

      # 省略

      # パスワードの文字数テスト ▼

      it "passwordが7文字以上であれば登録できること" do
        user = build(:user, password: "1234567", encrypted_password: "1234567") # buildメソッドを使用して7文字のパスワードを設定
        expect(user).to be_valid
      end
      
      it "passwordが7文字以下であれば登録できないこと" do
        user = build(:user, password: "123456", encrypted_password: "123456") # 意図的に6文字のパスワードを設定してエラーが出るかをテスト 
        user.valid?
        expect(user.errors[:encrypted_password]).to include("は7文字以上で入力してください")
      end
  end
end
```

最後に全角入力と全角カナ入力のテストを行います。

```user.spec.rb
require 'rails_helper'

describe User do
  describe '#create' do

      # 省略

      # 名前全角入力のテスト ▼

      it 'family_nameが全角入力でなければ登録できないこと' do
        user = build(:user, family_name: "ｱｲｳｴｵ") # 意図的に半角入力を行いエラーを発生させる
        user.valid?
        expect(user.errors[:family_name]).to include("は不正な値です")
      end

      it 'first_nameが全角入力でなければ登録できないこと' do
        user = build(:user, first_name: "ｱｲｳｴｵ") # 意図的に半角入力を行いエラーを発生させる
        user.valid?
        expect(user.errors[:first_name]).to include("は不正な値です")
      end

      # カタカナ全角入力のテスト ▼

      it 'family_name_kanaが全角カタカナでなければ登録できないこと' do
        user = build(:user, family_name_kana: "あいうえお") # 意図的にひらがな入力を行いエラーを発生させる
        user.valid?
        expect(user.errors[:family_name_kana]).to include("は不正な値です")
      end
      
      it 'first_name_kanaが全角カタカナでなければ登録できないこと' do
        user = build(:user, first_name_kana: "あいうえお") # 意図的にひらがな入力を行いエラーを発生させる
        user.valid?
        expect(user.errors[:first_name_kana]).to include("は不正な値です")
      end
  end
end
```

これで仕様書に沿った機能のテストは以上です。
最終的には以下のようになります。

```user.spec.rb
require 'rails_helper'

describe User do
  describe '#create' do

      # 入力されている場合のテスト ▼

      it "全ての項目の入力が存在すれば登録できること" do
        user = build(:user)
        expect(user).to be_valid
      end

      # nul:false, presence: true のテスト ▼

      it "nicknameがない場合は登録できないこと" do
        user = build(:user, nickname: nil)
        user.valid?
        expect(user.errors[:nickname]).to include("を入力してください")
      end

      it "emailがない場合は登録できないこと" do
        user = build(:user, email: nil)
        user.valid?
        expect(user.errors[:email]).to include("を入力してください")
      end

      it "passwordがない場合は登録できないこと" do
        user = build(:user, password: nil)
        user.valid?
        expect(user.errors[:password]).to include("を入力してください")
      end

      it "encrypted_passwordがない場合は登録できないこと" do
        user = build(:user, encrypted_password: nil)
        user.valid?
        expect(user.errors[:encrypted_password]).to include("を入力してください")
      end

      it "family_nameがない場合は登録できないこと" do
        user = build(:user, family_name: nil)
        user.valid?
        expect(user.errors[:family_name]).to include("を入力してください")
      end

      it "family_name_kanaがない場合は登録できないこと" do
        user = build(:user, family_name_kana: nil)
        user.valid?
        expect(user.errors[:family_name_kana]).to include("を入力してください")
      end

      it "first_nameがない場合は登録できないこと" do
        user = build(:user, first_name: nil)
        user.valid?
        expect(user.errors[:first_name]).to include("を入力してください")
      end

      it "first_name_kanaがない場合は登録できないこと" do
        user = build(:user, first_name_kana: nil)
        user.valid?
        expect(user.errors[:first_name_kana]).to include("を入力してください")
      end

      it "birth_dayがない場合は登録できないこと" do
        user = build(:user, birth_day: nil)
        user.valid?
        expect(user.errors[:birth_day]).to include("を入力してください")
      end
      
      # パスワードの文字数テスト ▼

      it "passwordが7文字以上であれば登録できること" do
        user = build(:user, password: "1234567", encrypted_password: "1234567")
        expect(user).to be_valid
      end
      
      it "passwordが7文字以下であれば登録できないこと" do
        user = build(:user, password: "123456", encrypted_password: "123456")
        user.valid?
        expect(user.errors[:encrypted_password]).to include("は7文字以上で入力してください")
      end

      # email 一意性制約のテスト ▼

      it "重複したemailが存在する場合登録できないこと" do
        user = create(:user)
        another_user = build(:user, email: user.email)
        another_user.valid?
        expect(another_user.errors[:email]).to include("はすでに存在します")
      end

      # 確認用パスワードが必要であるテスト ▼
      
      it "passwordが存在してもencrypted_passwordがない場合は登録できないこと" do
        user = build(:user, encrypted_password: "")
        user.valid?
        expect(user.errors[:encrypted_password]).to include("を入力してください", "は7文字以上で入力してください")
      end

      # 本人確認名前全角入力のテスト ▼

      it 'family_nameが全角入力でなければ登録できないこと' do
        user = build(:user, family_name: "ｱｲｳｴｵ")
        user.valid?
        expect(user.errors[:family_name]).to include("は不正な値です")
      end

      it 'first_nameが全角入力でなければ登録できないこと' do
        user = build(:user, first_name: "ｱｲｳｴｵ")
        user.valid?
        expect(user.errors[:first_name]).to include("は不正な値です")
      end

      # 本人確認カタカナ全角入力のテスト ▼

      it 'family_name_kanaが全角カタカナでなければ登録できないこと' do
        user = build(:user, family_name_kana: "あいうえお")
        user.valid?
        expect(user.errors[:family_name_kana]).to include("は不正な値です")
      end
      
      it 'first_name_kanaが全角カタカナでなければ登録できないこと' do
        user = build(:user, first_name_kana: "あいうえお")
        user.valid?
        expect(user.errors[:first_name_kana]).to include("は不正な値です")
      end
  end
end
```

##　テスコードの実行
テストを記述したアプリのディレクトリのターミナルにて下記コマンドを実行。

```ターミナル.
$ bundle exec rspec         
```

すると下記画像のように処理が行われればテストが成功しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/e16012dd-c6e9-f662-fb2d-7fa2b2aeb0ef.png)

## 最後に
モデルの単体テストは基本文法と、エラーを発生させる処理と発生させない処理を把握し、それぞれのメソッドを理解することができれば新たに機能が追加された場合でもコードを記述できると思います。

何か気になる点がございましたらコメント欄にてお申し付けください！
最後までご覧いただきありがとうございました！