自作アプリのUserモデルにテストコード（RSpec）を書いてみた

# はじめに

Railsでよく見るバリデーションのテストをRSpecで書きました。

# テスト内容

今回は、name, user_id, email を持つUserモデルを作成しました。

```rb:マイグレーションファイル
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name
      t.string :user_id
      t.string :email

      t.timestamps
    end
  end
end
```

Userモデルのテストでは、主にバリデーションのテストを行いました。

```rb:app/models/user.rb
class User < ApplicationRecord
  before_save { email.downcase! }
  validates :name, presence: true,
                   length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true,
                    length: { maximum: 255 },
                    uniqueness: { case_sensitive: false },
                    format: { with: VALID_EMAIL_REGEX }
end
```

バリデーションの設定に関しては、[Railsチュートリアル](https://railstutorial.jp/chapters/modeling_users?version=5.1#cha-modeling_users)を参考にしています。バリデーションの設定や意味などは、そちらを参考に。

# 環境設定

Gemfile に以下のように追記します。

```rb
group :development, :test do
  # 省略
  gem 'factory_bot_rails'
  gem 'rspec-rails'
  # 省略
end
```

そうすると、Userモデル作成と同時にRSpecのファイルが生成されます。

```shell
$ rails g model User ...

Running via Spring preloader in process 19654
      invoke  active_record
      create    db/migrate/20190617xxxxxx_create_userss.rb
      create    app/models/user.rb
      invoke    rspec
      create      spec/models/user_spec.rb
```

なので、テストは以下に書き込めばいいと。

`spec/models/user_spec.rb`

# テスト作成

以下が、Userモデルに対するすテスト（RSpec）

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

RSpec.describe User, type: :model do
  before do
    @user = User.create(
      name:    "Yamada Taro",
      user_id: "taro",
      email:   "taro@example.com",
    )
  end

  # name、user_id、emailがあれば有効な状態であること
  it "is valid with a name, user_id, email" do
    user = User.new(
      name:    "Sato Taro",
      user_id: "sato",
      email:   "sato@example.com",
    )
    expect(user).to be_valid
  end

  # nameがなければ無効な状態であること（nameは必須項目）
  it "is invalid without a name" do
    user = User.new(name: nil)
    user.valid?
    expect(user.errors[:name]).to include("can't be blank")
  end

  # メールアドレスがなければ無効な状態であること（emailは必須項目）
  it "is invalid without an email address" do
    user = User.new(email: nil)
    user.valid?
    expect(user.errors[:email]).to include("can't be blank")
  end

  # 重複したメールアドレスなら無効な状態であること
  it "is invalid with a duplicate email address" do
    user = User.new(email: "taro@example.com")
    user.valid?
    expect(user.errors[:email]).to include("has already been taken")
  end

  # メールアドレスが小文字化されていること
  it "email addresses should be saved as lower-case" do
    mixed_case_email = "TANAKA@example.com"
    @user.email = mixed_case_email
    @user.save
    expect(@user[:email]).to eq("tanaka@example.com")
  end

  # メールアドレスの一意性の検証
  it "email is unique" do
    user = User.create(name: "Test", user_id: "test", email: "taro@example.com")
    expect(user).to_not be_valid
    expect(user.errors[:email]).to include("has already been taken")
  end

  # メールアドレスのフォーマットの検証
  describe "mail" do
    context "Correct format" do
      it "is OK" do
        @user.email = 'user@example.com'
        expect(@user).to be_valid
      end
    end
    context "Incorrect format" do
      it "is NG" do
        @user.email = 'user@example,com'
        expect(@user).to_not be_valid
        expect(@user.errors[:email]).to include("is invalid")
      end
    end
  end

  # nameの長さの検証
  describe "name length" do
    context "length 50 txt" do
      it "is OK" do
        @user.name = 'a' * 50
        expect(@user).to be_valid
      end
    end
    context "length 51 txt" do
      it "is NG" do
        @user.name = 'a' * 51
        expect(@user).to_not be_valid
        expect(@user.errors[:name]).to include("is too long (maximum is 50 characters)")
      end
    end
  end
end
```

## 1ブロックづつ説明します

重複のチェックの際に使用するので、事前にテスト対象のオブジェクトとしてインスタンス化しておきます。

```rb:spec/models/user_spec.rb
before do
  @user = User.create(
    name:    "Yamada Taro",
    user_id: "taro",
    email:   "taro@example.com",
  )
end
```

# name、user_id、emailがあれば有効な状態であること

```rb:spec/models/user_spec.rb
  # name、user_id、emailがあれば有効な状態であること
  it "is valid with a name, user_id, email" do
    user = User.new(
      name:    "Sato Taro",
      user_id: "sato",
      email:   "sato@example.com",
    )
    expect(user).to be_valid
  end
```

`expect(user).to be_valid` は、`user` が妥当（valid）なことを期待するという意味です。

RSpecではこうしたエクスペクテーション(expect)を書くため、`to`や`to_not`を用いています。

# nameがなければ無効な状態であること（nameは必須項目）

```rb:spec/models/user_spec.rb
it "is invalid without a name" do
  user = User.new(name: nil)
  user.valid?
  expect(user.errors[:name]).to include("can't be blank")
end
```

テストでは、`user.name`にエラーメッセージ（`"can't be blank"`）が付いてることを期待(expect)しています。

# メールアドレスがなければ無効な状態であること（emailは必須項目）

```rb:spec/models/user_spec.rb  
it "is invalid without an email address" do
  user = User.new(email: nil)
  user.valid?
  expect(user.errors[:email]).to include("can't be blank")
end
```

valid? が `false` なら、`errors.messages`にエラー内容が格納されます。今回は、それが、`"can't be blank"`であることを期待(expect)しています。


# 重複したメールアドレスなら無効な状態であること

```rb:spec/models/user_spec.rb
it "is invalid with a duplicate email address" do
  user = User.new(email: "taro@example.com")
  user.valid?
  expect(user.errors[:email]).to include("has already been taken")
end
```

valid? が `false` なら、`errors.messages`にエラー内容が格納されます。今回は、それが、`"has already been taken"`であることを期待(expect)しています。

# メールアドレスが小文字化されていること

```rb:spec/models/user_spec.rb
it "email addresses should be saved as lower-case" do
  mixed_case_email = "TANAKA@example.com"
  @user.email = mixed_case_email
  @user.save
  expect(@user[:email]).to eq("tanaka@example.com")
end
```

`@user[:email]`の値 = `"tanaka@example.com"` が`TRUE` であることを期待(expect)しています。

# メールアドレスの一意性の検証

```rb:spec/models/user_spec.rb
it "email is unique" do
  user = User.create(name: "Test", user_id: "test", email: "taro@example.com")
  expect(user).to_not be_valid
  expect(user.errors[:email]).to include("has already been taken")
end
```

`taro@example.com`はすでに保存されたアドレスなので、バリデーションは通らないことと、エラーメッセージは`"has already been taken"`であることを期待(expect)しています。

# メールアドレスのフォーマットの検証

- `,com`のカンマがあるのは、無効である
- `.com`であれば、有効である（すくなくとも無効ではない）

ことを

`User`モデルのバリデーション部分に記載しているので、テストがパスしないことを期待(expect)しています。

```rb:spec/models/user_spec.rb
describe "mail" do
  context "Correct format" do
    it "is OK" do
      @user.email = 'user@example.com'
      expect(@user).to be_valid
    end
  end
  context "Incorrect format" do
    it "is NG" do
      @user.email = 'user@example,com'
      expect(@user).to_not be_valid
      expect(@user.errors[:email]).to include("is invalid")
    end
  end
end
```

# nameの長さの検証

`name`の中身の文字数が

- 50文字までは有効
- 51文字以上は無効

であることを`User`モデルのバリデーション部分に記載しているので、テストがパスしないことを期待(expect)しています。

```rb:spec/models/user_spec.rb
describe "name length" do
  context "length 50 txt" do
    it "is OK" do
      @user.name = 'a' * 50
      expect(@user).to be_valid
    end
  end
  context "length 51 txt" do
    it "is NG" do
      @user.name = 'a' * 51
      expect(@user).to_not be_valid
      expect(@user.errors[:name]).to include("is too long (maximum is 50 characters)")
    end
  end
end
```

# テスト実行結果

`bundle exec rspec spec` で実行できます。

```bash

$ bundle exec rspec spec
                                                                                                                                                                                                   
StaticPagesController
  GET #top
    returns http success
  GET #about
    returns http success

UsersController
  GET #new
    returns http success

User
  is valid with a name, user_id, email
  is invalid without a name
  is invalid without an email address
  is invalid with a duplicate email address
  email addresses should be saved as lower-case
  email is unique
  mail
    Correct format
      is OK
    Incorrect format
      is NG
  name length
    length 50 txt
      is OK
    length 51 txt
      is NG

Finished in 0.34401 seconds (files took 1.19 seconds to load)
13 examples, 0 failures
```

全て通りました。

# まとめ

- テストとは基本的に、期待する値と、実際に生成される値を比較検証であると実感
- テストの妥当性を検証するために、期待する値を変更したりして、テストをパスしないことを確かめる必要性を実感
- `it ... end` 中にあるブロックは、1つのテストをまとめる役割をしています。これによって、テストがパスしなかった場合、どこでコケたか、原因究明がわかりやすくなる
- テストをパスするということは、デプロイのたびにビクビクせず、自信を持てると実感

まだまだ勉強中なので、問題点があればご指摘ください。

# 参考

- https://qiita.com/jnchito/items/42193d066bd61c740612
- https://qiita.com/uchiko/items/d34c5d1298934252f58f