投稿アプリのモデルの単体テストコード

テストコードは本当に理解できず一周目は正直飛ばしてましたが
復習してなんとなく理解できました！
というわけで復習の備忘録です！

目的
- Userモデルの単体テストの実装を通して、正常系テストを理解
- Userモデルの単体テストの問題を解いて、異常系テストに関する知識を定着
- Tweetモデルの単体テストの問題を解いて、モデル単体テストに関する知識を定着

####文字数制限のバリデーションを設置
「保存できる値は最大６文字まで」というバリデーションを設置
app/models/user.rb
```ruby
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
        :recoverable, :rememberable, :validatable
  has_many :tweets
  has_many :comments  # commentsテーブルとのアソシエーション

  validates :nickname, presence: true, length: { maximum: 6 }
end
```

#### フォームに設けている文字数制限を削除
app/views/devise/registrations/new.html.erb
```ruby
<div class="contents row">
  <div class="container">
    <h2>Sign up</h2>
    <%= form_with model: @user, url: user_registration_path, id: 'new_user', class: 'new_user', local: true do |f| %>
      <%= render "devise/shared/error_messages", resource: resource %>

      <div class="field">
        <%= f.label :nickname %> <em>(6 characters maximum)</em><br />
        <%= f.text_field :nickname, autofocus: true %>
      </div>

<%# 以下省略 %>
```

Userのモデル単体テストの実装
#### exampleを整理
モデル単体テストはバリデーションやメソッドの検証です
バリデーションは「属性の値がDBに保存されても良いかチェックを行う条件」を設定
`presence`は、指定した属性の値の有無をチェック
Userモデルでは、`nickname`に対して、`presence:true`の条件を指定しています
この場合、DBに保存する前にnicknameに値が存在していなければ、条件に引っ掛かりDBに保存されません
`length` は、指定した属性の値の文字数制限をチェックします
Userモデルでは、`nickname`に対して、`length: { maximum: 6 }` の条件を指定しています。

- nicknameとemail、passwordとpassword_confirmationが存在すれば登録できる
- nicknameが空では登録できない
- emailが空では登録できない
- passwordが空では登録できない
- passwordとpassword_confirmationが不一致では登録できない
- nicknameが7文字以上では登録できない
- 重複したemailが存在する場合は登録できない
- emailは@を含まないと登録できない
- passwordが5文字以下では登録できない
- passwordが129文字以上では登録できない

### 新規登録できる場合のテストコードを記述
1. 検証したいモデルのインスタンスを生成
1. 生成したインスタンスがどのような状況であればいいか記述
#### be_valid
`valid?`のメソッドの返り値が、`true`であることを期待するマッチャ
```
% rails c
[1] pry(main)> @user = FactoryBot.build(:user)
  (4.3ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
=> #<User id: nil, email: "judie@gmail.com", created_at: nil, updated_at: nil, nickname: "GP">
[2] pry(main)> @user.valid?
  User Exists? (10.3ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'judie@gmail.com' LIMIT 1
=> true
```

spec/models/user_spec.rb
```ruby
it 'nicknameとemail、passwordとpassword_confirmationが存在すれば登録できる' do
  expect(@user).to be_valid
end
```

テストコード
`bundle exec rspec spec/models/user_spec.rb`

### 新規登録できない場合のテストコードを記述

spec/models/user_spec.rb
```ruby
it 'nicknameが空で登録できない' do
      @user.nickname = ''
      @user.valid?
      expect(@user.errors.full_messages).to include("Nickname 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 'passwordが空では登録できない' do
      @user.password = ''
      @user.valid?
      expect(@user.errors.full_messages).to include("Password can't be blank")
    end
    it 'passwordとpassword_confirmationが不一致では登録できない' do
      @user.password = '123456'
      @user.password_confirmation = '1234567'
      @user.valid?
      expect(@user.errors.full_messages).to include("Password confirmation doesn't match Password")
    end
    it 'nicknameが7文字以上では登録できない' do
      @user.nickname = 'aaaaaaa'
      @user.valid?
      expect(@user.errors.full_messages).to include('Nickname is too long (maximum is 6 characters)')
    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 = 'testmail'
      @user.valid?
      expect(@user.errors.full_messages).to include('Email is invalid')
    end
    it 'passwordが5文字以下では登録できない' do
      @user.password = '00000'
      @user.password_confirmation = '00000'
      @user.valid?
      expect(@user.errors.full_messages).to include('Password is too short (minimum is 6 characters)')
    end
    it 'passwordが129文字以上では登録できない' do
      @user.password = Faker::Internet.password(min_length: 129, max_length: 150)
      @user.password_confirmation = @user.password
      @user.valid?
      expect(@user.errors.full_messages).to include("Password is too long (maximum is 128 characters)")
    end
  end
end
```

#### context
特定の条件を指定してグループを分けます
使用方法は`describe`と同じですが、`describe`には何についてのテストなのかを指定するのに対し、`cotext`には特定の条件を指定

```ruby
require 'rails_helper'

RSpec.describe User, type: :model do
  before do
    @user = FactoryBot.build(:user)
  end

  describe 'ユーザー新規登録' do
    context '新規登録できるとき' do
      it 'nicknameとemail、passwordとpassword_confirmationが存在すれば登録できる' do
        expect(@user).to be_valid
      end
    end
    context '新規登録できないとき' do
      it 'nicknameが空で登録できない' do
        @user.nickname = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Nickname 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 'passwordが空では登録できない' do
        @user.password = ''
        @user.valid?
        expect(@user.errors.full_messages).to include("Password can't be blank")
      end
      it 'passwordとpassword_confirmationが不一致では登録できない' do
        @user.password = '123456'
        @user.password_confirmation = '1234567'
        @user.valid?
        expect(@user.errors.full_messages).to include("Password confirmation doesn't match Password")
      end
      it 'nicknameが7文字以上では登録できない' do
        @user.nickname = 'aaaaaaa'
        @user.valid?
        expect(@user.errors.full_messages).to include('Nickname is too long (maximum is 6 characters)')
      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 = 'testmail'
        @user.valid?
        expect(@user.errors.full_messages).to include('Email is invalid')
      end
      it 'passwordが5文字以下では登録できない' do
        @user.password = '00000'
        @user.password_confirmation = '00000'
        @user.valid?
        expect(@user.errors.full_messages).to include('Password is too short (minimum is 6 characters)')
      end
      it 'passwordが129文字以上では登録できない' do
        @user.password = Faker::Internet.password(min_length: 129, max_length: 150)
        @user.password_confirmation = @user.password
        @user.valid?
        expect(@user.errors.full_messages).to include("Password is too long (maximum is 128 characters)")
      end
    end
  end
```

### Tweetモデル単体テストコードの記述
#### テストコードを記述するファイルを作成

`rails g rspec:model tweet`

### FactoryBotを準備
spec/factories/tweets.rb
```ruby
FactoryBot.define do
  factory :tweet do
    text {Faker::Lorem.sentence}
    image {Faker::Lorem.sentence}
    association :user 
  end
end
```

`association :user`は、`user.rb`のFactoryBotとアソシエーションがあることを意味してる
Tweetのインスタンスが生成したと同時にm関連するUserのインスタンスも生成されます
Tweetに対しては、必ずUserが紐づいている必要があるため、このように記述する必要がある
（UserはTweetを必ず持っているわけではないため、User.rbには記述しません）

### exampleの整理
- 画像とテキストを投稿できる
- テキストのみで投稿できる
- テキストが空では投稿できない
- ユーザーが紐付いていなければ投稿できない

```ruby
require 'rails_helper'

RSpec.describe Tweet, type: :model do
  before do
    @tweet = FactoryBot.build(:tweet)
  end

  describe 'ツイートの保存' do 
    context 'ツイートが投稿できる場合' do
      it '画像とテキストを投稿できる' do
        expect(@tweet).to be_valid
      end
      it 'テキストのみ投稿できる' do
        @tweet.image = ''
        expect(@tweet).to be_valid
      end
    end
    context 'ツイートが投稿できない場合' do
      it 'テキストが空では投稿できない' do
        @tweet.text = ''
        @tweet.valid?
        expect(@tweet.errors.full_messages).to include("Text can't be blank")
      end
      it 'ユーザーが紐づいてなければ投稿できない' do
        @tweet.user = nil
        @tweet.valid?
        expect(@tweet.errors.full_messages).to include('User must exist')
      end
    end
  end
end

```