RSpec - 'users validation'のテストコードをレビューしてもらった結果

# はじめに
みなさん、こんにちわ！Qiitaの投稿、ruby、rails、rspec初心者の J です！
つい先日、「プロから見た僕のspecはどうなの？」というのが気になり、
[RSpec - 'users validation'のテスト](https://qiita.com/denjneb/items/36f0f86271b5ebd1a4d1#comment-9fb76aed6af4ca75cea5) を質問として、投稿しました。
そして、rubyやrspecについての動画をyoutubeで配信されていて、
僕の先生である@jnchito さんにコードレビューをしてもらいました！
テストコードは、そこそこ長かったのですが、一つ一つ徹底的に指摘してもらえて、
「僕ならこう書く！」を丁寧に解説してくださいました！
おかげさまで、
>「ルールに従ったテストコードがなんなのか」と迷子になり、
なかなかcommitできず、地獄を彷徨っています。

から開放されて今は天にも上ような気分です！、、レビュー頂いて本当にありがとうございます。。泣

### @jnchito さんについて
有名なので、みなさん知ってるかもですが、、

- [プロを目指す人のためのRuby入門](https://www.amazon.co.jp/dp/B077Q8BXHC/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)  
rubyについての書籍を出していたり、、
- [Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp)  
rspecについての書籍の和訳をしたり、、
- [正規表現でテキスト中のTwitterアカウントだけを抜き出してみる](https://www.youtube.com/watch?v=wfNzYXRNbtg)  
youtubeで動画配信したり、、勉強会を開催している動画もありました！
- [（1/2）Qiitaに投稿されていたRSpecのテストコードをレビューしてみた](https://www.youtube.com/watch?v=1wtS8NcCrGg)  
今回のレビューしてもらった動画はこちら！

めっちゃかっこいいダンディな先生です！！

という訳でこの記事は、僕自信がスペッカーになるためのアウトプットでもありますが、
まだ僕と同じくらいのスキル感の同志たちの参考になるべく、
指摘・解説してもらった内容から再度、自分自身のコードを見返してダメなところをガチガチに直してみました！
よかったら参考にしてみてください！

### 修正前の僕のゴミコード
前回のコードとのbefore、afterテストコードを載せるとQiitaのデータベースがパンクしてしまうかも　←(・・。)
なので、 [修正前の僕のゴミコード](https://qiita.com/denjneb/items/36f0f86271b5ebd1a4d1#comment-9fb76aed6af4ca75cea5) を比較する場合は、タブを複製してどうぞ！

# Specのネストイメージ
「ネストが深すぎなくていいんじゃないか？」って訳で、なるべく浅く修正！

```ruby
RSpec.describe User, type: :model do
  describe 'attribute: name'
    context 'when present'
      it 'is valid'
        # ...省略
      end
    end
  end
end
```

上の内容は、、`User > name > when present > it is valid`
英訳的にはおかしな感じになりますが、どのネストも、、
「ユーザー名が存在する場合は有効です」的なニュアンスでわかりやすくネストし直しました。
これはあくまでイメージです！

# 修正後
## Userモデル

```ruby:app/models/user.rb
class User < ApplicationRecord
  before_save :downcase_email

  validates :name,
            presence: true,
            length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email,
            presence: true,
            length: { maximum: 255 },
            format: { with: VALID_EMAIL_REGEX },
            uniqueness: { case_sensitive: false }

  private

    def downcase_email
      self.email = self.email.downcase
    end
end
```

### 補足
- `VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i`  
[railstutorial](https://railstutorial.jp/chapters/modeling_users?version=5.1#sec-format_validation)より
「２つの連続したドットはマッチさせないようにする」こちらの正規表現に修正しました。
- `self.email.downcase!`と、破壊的メソッドはあまり使用しない。  
将来的にいつどこで参照されるか不明なので、破壊的メソッドを使用しない方が無難。  
、、、たしかに。（ということで修正済みが以下。）

```ruby
def downcase_email
  self.email = self.email.downcase
end
```

## FactoryBot
```ruby:spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'Tony Stark' }
    email { 'tony@example.com' }
  end
end
```
### 補足
- 修正前: `name { 'hoge' }`、 `email { "foo@bar.com" }`  
ありえない思っていたメールアドレスのドメイン部分。。あ、あれ？  
[bar.com](https://www.bar.com/)　ポチッ！  
、、、`@example.com`を使用すると誓います。


## Spec

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

RSpec.describe User, type: :model do
  let(:user) { FactoryBot.build(:user) }

  describe 'attribute: name' do
    context 'when present' do
      # nameが存在している場合は、有効であること
      it 'is valid' do
        user.name = 'Tony Stark'
        expect(user).to be_valid
      end
    end

    context 'when blank' do
      # nameが空白の場合は、無効であること
      it 'is invalid' do
        user.name = ' '
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end

    context 'when empty' do
      # nameが空の場合は、無効であること
      it 'is invalid' do
        user.name = ''
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end

    context 'when nil' do
      # nameが存在していない場合は、無効であること
      it 'is invalid' do
        user.name = nil
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end

    context 'when length is 50 characters or less' do
      # nameが50文字以下の場合は、有効であること
      it 'is valid' do
        user.name = 'a' * 50
        expect(user).to be_valid
      end
    end

    context 'when length is more than 50 characters' do
      # nameが50文字を超える場合は、無効であること
      it 'is invalid' do
        user.name = 'a' * 51
        expect(user).to be_invalid
        expect(user.errors[:name]).to include('is too long (maximum is 50 characters)')
      end
    end
  end

  describe 'attribute: email' do
    context 'when present' do
      # emailが存在している場合は、有効であること
      it 'is invalid' do
        user.email = 'tony@example.com'
        expect(user).to be_valid
      end
    end

    context 'when blank' do
      # emailが空白の場合は、無効であること
      it 'is invalid' do
        user.email = ' '
        expect(user).to be_invalid
        expect(user.errors[:email]).to include("can't be blank")
      end
    end

    context 'when empty' do
      # emailが空の場合は、無効であること
      it 'is invalid' do
        user.email = ''
        expect(user).to be_invalid
        expect(user.errors[:email]).to include("can't be blank")
      end
    end

    context 'when nil' do
      # emailが存在していない場合は、無効であること
      it 'is invalid' do
        user.email = nil
        expect(user).to be_invalid
        expect(user.errors[:email]).to include("can't be blank")
      end
    end

    context 'when length is 50 characters or less' do
      # emailが255文字以下の場合は、有効であること
      it 'is valid' do
        user.email = 'a' * 243 + '@example.com'
        expect(user).to be_valid
      end
    end

    context 'when length is more than 50 characters' do
      # emailが255文字を超える場合は、無効であること
      it 'is invalid' do
        user.email = 'a' * 244 + '@example.com'
        expect(user).to be_invalid
        expect(user.errors[:email]).to include('is too long (maximum is 255 characters)')
      end
    end

    context 'when correct format' do
      # emailの形式が正しい場合は、有効であること
      it 'is valid' do
        user.email = 'user@example.com'
        expect(user).to be_valid

        user.email = 'USER@foo.COM'
        expect(user).to be_valid

        user.email = 'A_US-ER@foo.bar.org'
        expect(user).to be_valid

        user.email = 'foo.bar@baz.jp'
        expect(user).to be_valid

        user.email = 'foo+bar@baz.cn'
        expect(user).to be_valid
      end
    end

    context 'when is incorrect format' do
      # emailの形式が正しくない場合は、無効であること
      it 'is invalid' do
        user.email = 'user@example,com'
        expect(user).to be_invalid

        user.email = 'user_at_foo.org'
        expect(user).to be_invalid

        user.email = 'user.name@example.'
        expect(user).to be_invalid

        user.email = 'foo@bar_baz.com'
        expect(user).to be_invalid

        user.email = 'foo@bar+baz.com'
        expect(user).to be_invalid
      end
    end

    context 'when already taken' do
      # 同一のemailが既に登録されている場合は、無効であること
      it 'is invalid' do
        FactoryBot.create(:user, email: 'tony@example.com')
        user.email = 'tony@example.com'
        expect(user).to be_invalid
        expect(user.errors[:email]).to include('has already been taken')
      end
    end

    context 'when case insensitive and not unipue' do
      # emailの大文字と小文字を区別せず、一意ではない場合は、無効であること
      it 'is invalid' do
        FactoryBot.create(:user, email: 'tony@example.com')
        user.email = 'TONY@EXAMPLE.COM'
        expect(user).to be_invalid
        expect(user.errors[:email]).to include('has already been taken')
      end
    end

    # emailが小文字で保存されていること
    it 'is saved in lowercase' do
      user.email = 'TONY@EXAMPLE.COM'
      user.save!
      expect(user.reload.email).to eq 'tony@example.com'
    end
  end
end
```

### 補足
- 先生：必要ないデバックコードは消しましょう。  
僕：あ、はい！でもテストしてるときに`pry`は便利です！  
- 先生：`FactoryBot`のユーザーを見にいかないといけないのはよくないなぁ  
僕：`FactoryBot`でユーザーを定義している場合でも、エクスペクテーションを書く前に明確にnameに値を入れます！  
- 僕：`context`、`it`の文章を修正しました！  
  - context: 'when ~' ~である時をグループ化  
  - it: validパターンといinvalidパターンを検証  
イメージはネストイメージの部分の文脈のニュアンスです。  
- 先生：saveの検証まではいらないかなぁ。。  
validationが通ってsaveで落ちることは、滅多にないし、もしあったらrailsのバグか何か。  
僕：、、、なるほど！（削除済み以下。）  
修正前のコードを抜粋: `expect(user.save).to be_falsey`  
- 先生：ここのテストをまとめよう。  
「nameが存在していない場合は、無効であること」  
  - `name = ' '`（スペースのみ）  
  - `name = ''`（空）  
  - `name = nil`（nil）  

```ruby
    context 'when name is blank' do
      # nameが存在しない場合は、無効であること
      it 'is invalid' do
        user.name = ' '
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")

        user.name = ''
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")

        user.name = nil
        expect(user).to be_invalid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end
```

僕：ここはなんとなくまとめずに書きたいと思ったので`blank`、`empty`、`nil`で区分けしました！

- 僕：`user.valid?`と聞いてあげないとエラーメッセージにmessagesが入ってこないと思ってました、、  
ということで`user.valid?`と聞かずに`expect`をそのまま記載！  
- 先生：正規表現のテスト、ここはいいですね！  
僕：あざっす！  
- 先生：dryを捨てて、変数に格納してあれやこれやしない！  
ロジックを組むと、そのロジックにミスが発生して思わぬ事故になる。。。  
- 先生：`uniqueness: { case_sensitive: false }`をテストした方がいいのでは？  
僕：[（2/2）Qiitaに投稿されていたRSpecのテストコードをレビューしてみた](https://www.youtube.com/watch?v=B3-e6GZ4vzU&t=657s)で解説してもらったように、、
  - emailアドレスが小文字の有効なuserを保存。
  - 上で登録したuserの大文字emailをuserのemailに入れ直す。
  - userは無効であること。
  - また、エラーメッセージに`has already been taken`が含まれていること。
が以下のコード。

```ruby
context 'when case insensitive and not unipue' do
  # emailの大文字と小文字を区別せず、一意ではない場合は、無効であること
  it 'is invalid' do
    FactoryBot.create(:user, email: 'tony@example.com')
    user.email = 'TONY@EXAMPLE.COM'
    expect(user).to be_invalid
    expect(user.errors[:email]).to include('has already been taken')
  end
end
```

# まとめ
もっとたくさんの指摘もらいましたが、まだプロの考えに及んでいない自分がいるので、
レビュー&解説して頂いた動画を見直して理解した部分をもっと明確に記載しようと思います！
テスト書き始めの方は、僕と同じで「どこまでテスト」していいのかわからないとなる同志も少なくなさそうです。
それぞれ意見は異なると思いますが、初心者的にズバリ！
## 'railsのバリデーションを信じて、自分や他の人の作ったロジックをテストしよう！'
ってことですね！
railsの検証をわざわざテストするのは、もったいないと思いました、、
また、先生も言っていた通り、「言い出したらキリがないもの」もあるということを考えれば、
テストポイントを絞る力の方が重要だと思います。
今回のバリデーションに対するテストは、ほぼ100%と言える正解のテストコードが存在すると思うので、
自分やチームメンバーが作ったロジックをテストする未来の練習にはなりそうです！

それからみなさん！
## 必要でない限り、テストはdryを捨てましょう！
みんながコード読みやすくてハッピーになれるし、
何より、、
TESTが間違ってたら意味ないですからね、、w

では、また！

# テストのアウトプット
```
User
  attribute: name
    when present
      is valid
    when blank
      is invalid
    when empty
      is invalid
    when nil
      is invalid
    when length is 50 characters or less
      is valid
    when length is more than 50 characters
      is invalid
  attribute: email
    is saved in lowercase
    when present
      is invalid
    when blank
      is invalid
    when empty
      is invalid
    when nil
      is invalid
    when length is 50 characters or less
      is valid
    when length is more than 50 characters
      is invalid
    when correct format
      is valid
    when is incorrect format
      is invalid
    when already taken
      is invalid
    when case insensitive and not unipue
      is invalid
```