【RSpec on Rails】初心者による初心者のためのテストコードの書き方

今回テストコードへの理解を深めるため私なりにRSpecを使ったテストコードの書き方をまとめていきます。  初めてテストコードを書いたときはなぜテストコードが必要かもよく分からずに見様見真似で書いていたのを覚えていますので初心に返りできる限り詳細に書いていきたいと思います。ちなみに用語の詳細な説明は省いています。
わかりやすくまとめてくれている記事があるのでご覧ください。
オススメ => [使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」
](https://qiita.com/jnchito/items/42193d066bd61c740612)


##1.なぜテストコードが必要なのか  
アプリケーションの動作確認をコンピュータに任せることで品質を保証するためです。  人が手動で動作確認をしているとどうしても抜け漏れが起こる可能性があります。  テストを自動化することで人為的なミスをなくすことができます。  他にも仕様の変更があった場合も変更分だけ確認すれば良いので全てのテストを1からやり直すという手間が省けます。  

##2.Rspecとは  
RSpec（アールスペック）は、Ruby on Railsのテストコードを書くために用いられるGemです。  Ruby on RailsにはMinitestという標準でテストする機能が備わっていますがRailsを使った実際の開発現場ではRSpecを使ったテストが主流らしいので練習として使っています。

##3.今回テストをするアプリケーションの内容について  
今回は自前のチャットアプリでUser, Room, Messageの3つのモデルがありそのUserモデル単体のテストコードの流れを書いていきます。  Userモデルの単体テストコードの目的はユーザー登録が正しくできるか、設定したバリデーションが機能しているかの確認になります。  

##4.開発環境  
・ macOS Catalina 10.15.6
・ Ruby 2.6.5
・ Rails 6.0.3
・ Gem FactoryBot
・ Gem RSpec
・ Gem Faker  

##5. 手順  
#####5-0. 準備
まずはFactoryBot、RSpec、Fakerを導入します。  

```ruby:Gemfile
(略)
group :development, :test do
  # 今回は最新版を使用するためバージョンは指定しません
  gem 'rspec-rails'
  # FactoryBotはインスタンスを自動生成するために使います。なくても大丈夫ですがこれも練習として。
  gem 'factory_bot_rails'
  # Fakerは名前とかパスワードをランダムに生成してくれます。これも便利なのでなれておきましょう。
  gem 'faker'
end
(略)
```
bundleインストールを実行

```
% bundle install
```  

#####5-1. RSpecの設定  
RSpecに必要なファイルを作成しましょう。

```
% rails g rspec:install
```
これでいくつかのファイルが作成されたと思います。

```
create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb
```
.rspecファイルに以下を記述しましょう

```ruby:.rspec
--require spec_helper
--format documentation
```
この記述によりターミナル上でテストコードの結果が見られるようになります。
以下のコマンドを実行し必要なUserモデルのテストのためのファイルを作成しましょう。
今回はUserモデルのみですが他のモデルのときも同じ手順です。

```terminal
% rails g rspec:model user
```
これで必要なファイルが生成されたと思います。

```
create  spec/models/user_spec.rb
invoke  factory_bot
create spec/factories/users.rb
```
もしusers.rbファイルが作成されない場合などあればspecディレクトリ内にfactoriesディレクトリを作成し、その中にusers.rbファイルを作成してください。

#####5-2. FactoryBotの設定  
FactoryBotの設定をしていきます。
spec/factories/users.rbに以下の記述をしましょう。

```ruby:spec/factories/users.rb

# FactoryBotを使用するための記述です
FactoryBot.define do
  # 以下のように記述することでUserクラス(Userモデル)だと自動で判断してくれます
  factory :user do
    # nameに保存する内容をランダムに生成します
    name {Faker::Name.first_name}
    # emailを自動で生成します
    email {Faker::Internet.email}
    # passwordは確認用も含め同じ値を2度入力しないといけないのでランダムに生成した値を変数化します
    # deviseのpasswordは6文字以上でないといけないのでmin_lengthで6文字以上に設定します
    password = Faker::Internet.password(min_length: 6)
    # passwordとパスワードの確認枠に上記で設定した変数を設定します
    password {password}
    password_confirmation {password}
  end
end

```  
今回はname, email, passwordのみですが他にバリデーションを設定している項目があれば追加してください。
これでFactoryBotの設定は完了です。  試しにターミナルでコンソールを起動し確認してみましょう。  

```terminal
% rails c

[1] pry(main)> FactoryBot.build(:user)
=> #<User id: nil, name: "颯", email: "darius@sipes.info", created_at: nil, updated_at: nil>
```
このように出力されればOKです!(name、emailはFakerによりランダムに生成されるので毎回変わります。)  

#####5-3. テストコードを書く  
それでは実際にテストコードを書いていきます。
まずはuser_spec.rbファイルを確認していきます.  

```diff:spec/models/spec_user.rb
require 'rails_helper'

RSpec.describe User, type: :model do
- pending "add some examples to (or delete) #{__FILE__}"
+ describe '#create' do
+   before do
+     @user = FactoryBot.build(:user)
+   end
+ end
end
```
まずはこのように書いていきます。
describeは説明するというような意味なので何に関するテストなのかを記述しています。
beforeの後のブロックにspec/factories/users.rbファイルで設定した内容を基にuserのインスタンスを作成し変数にします。
このあと異なるスコープで使用するのでインスタンス変数にします。

ここから簡単な解説をコード内に書いていきます

```ruby:spec/models/spec_user.rb
RSpec.describe User, type: :model do
  #ユーザーを新たに作成(登録)するテストなので#createとしています
  describe '#create' do
    # spec/factories/user.rbの内容を呼び出し変数にします
    # スコープが異なるのでローカル変数ではなく@をつけてインスタンス変数にします
    before do
      @user = FactoryBot.build(:user)
    end
    # contextには条件・状況を記述します
    context 'ユーザー登録ができる場合' do
      # itにはテストする項目を記述します
      it 'name, email, password, password_confirmationが正しく入力されていれば新規登録できる' do
        # FactoryBotで作成したuser情報が正しければバリデーションにかからずbe_validでテストをパスします
        expect(@user).to be_valid
      end
    end
  end
end
```
RSpecのテストコードの構文はcontextの後に条件を書くことが多いです。
今回のcontextの内容はif ~ else文でいうif(true)の部分だと考えてください。
そのため内容もユーザー登録ができる場合としています。
そしてexpect(X).to Yという形で期待されるテストの結果を記述していきます。
この辺りは以下の記事でとてもわかりやすく解説されています。

参考 => [使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」
](https://qiita.com/jnchito/items/42193d066bd61c740612)
今回はインスタンス変数に入っているuser情報がバリデーションにかからずに新規登録できるという結果を期待しています。
it ~ doの間にはテストする内容の詳細を書きましょう。
他の人が見ても何に関するテスト項目なのか分かるのが望ましいです。

ではテスト結果が正しいか確認してみましょう。
ターミナルで以下のコマンドを入力してください。

```
% bundle exec rspec spec/models/user_spec.rb 
```
ターミナルに以下のように出力されれば正しく登録できています。

```terminai
1 examples, 0 failure
```

次は登録できない場合をテストしていきます。
ちなみにテストには正常系と異常系があり、最初にテストしたユーザー新規登録をするというような本来の目的の機能を確かめるものを正常系、逆にバリデーションにかかって登録できないというような本来の目的を果たせないことを確かめることを異常系と言います。
このあとは異常系のテストを行っていきます。

以下のように記述しましょう。

```ruby:spec/models/user_spec.rb
 (略)
    context 'ユーザー登録ができる場合' do
      # itにはテストする項目を記述します
      it 'name, email, password, password_confirmationが正しく入力されていれば新規登録できる' do
        # FactoryBotで作成したuser情報が正しければバリデーションにかからずbe_validでテストをパスします
        expect(@user).to be_valid
      end
    end

    context 'ユーザー登録ができない場合' do
      # ユーザー名を入力しないで登録しようとした場合バリデーションにより登録できないことをテスト
      it 'nameが空だと登録できない' do
        # userのnameを空に更新
        @user.name = nil
        # valid?でバリデーションを通るか判定、通らないときはエラーメッセージを生成する
        @user.valid? 
        # @user.errors.full_messagesでエラーメッセージを表示させる
        # include以降に表示されたエラーメッセージを記述する
        # deviseを日本語化していなければName can't be blankというエラーメッセージになると思います
        expect(@user.errors.full_messages).to include("Nameを入力してください")
      end
    end
```

簡単に解説するとnameをnilにしてvalid?メソッドを使うことでバリデーションによるエラーメッセージを生成します。
Gemの'pry-rails'を導入していればuser.valid?の後にbinding.pryを記述し、テストコードを実行して処理が止まったところでコンソールにuser.errorsと入力するとエラーが起きていることが確認でき、その後続けてuser.erros.full_messagesと入力するとエラーメセージの確認もできます。
ちなみに上記コードのincludeの中の文字列が1文字でも違っていると以下のように出力されテストはパスしません。

```terminal
Failures:

1) User#create ユーザー登録ができない場合 nameが空だと登録できない
    Failure/Error: expect(@user.errors.full_messages).to include("Name can't be blank")
      expected ["Nameを入力してください"] to include "Name can't be blank"
    # ./spec/models/user_spec.rb:28:in `block (4 levels) in <top (required)>'

```
これはエラーメッセージは"Name can`t be blank"だと期待したけど、実際は"Nameを入力してください"だよという意味です。(deviseを日本語化していたのを忘れて素で間違えました、、、)
エラー文を確認するときはexpectedの後に実際のエラーメッセージが出力されるのでこちらをコピペすればテストはパスすると思います。

このあとは最終的なコードになります。

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

# describeには何のテストなのかを記述します
# 今回はUserモデルのテストなのでこのようになります
RSpec.describe User, type: :model do
  #ユーザーを新たに作成(登録)するテストなので#createとしています
  describe '#create' do
    # spec/factories/user.rbの内容を呼び出し変数にします
    # スコープが異なるのでローカル変数ではなく@をつけてインスタンス変数にします
    before do
      @user = FactoryBot.build(:user)
    end
    # contextには条件・状況を記述します
    context 'ユーザー登録ができる場合' do
      # itにはテストする項目を記述します
      it 'name, email, password, password_confirmationが正しく入力されていれば新規登録できる' do
        # FactoryBotで作成したuser情報が正しければバリデーションにかからずbe_validでテストをパスします
        expect(@user).to be_valid
      end
    end

    context 'ユーザー登録ができない場合' do
      # ユーザー名を入力しないで登録しようとした場合バリデーションにより登録できないことをテスト
      it 'nameが空だと登録できない' do
        # userのnameを空に更新
        @user.name = nil
        # valid?でバリデーションを通るか判定、通らないときはエラーメッセージを生成する
        @user.valid? 
        # @user.errors.full_messagesでエラーメッセージを表示させる
        # include以降に表示されたエラーメッセージを記述する
        # deviseを日本語化していなければName can't be blankというエラーメッセージになると思います
        expect(@user.errors.full_messages).to include("Nameを入力してください")
      end

      it 'emailが空だと登録できない' do
        @user.email = nil
        @user.valid?
        expect(@user.errors.full_messages).to include("Eメールを入力してください")
      end

      # deviseはdefaultでuniquenessという同じ内容は登録できないというバリデーションが設定されています
      it '同じemailはすでに使用されていると登録できない' do
        # uniquenessの確認のためユーザーを二人用意します
        # @userを保存します
        @user.save
        # 2人目のユーザーを作成します
        another_user = FactoryBot.build(:user)
        # another_userのemailを@userと同じemailに更新します
        another_user.email = @user.email
        another_user.valid?
        expect(another_user.errors.full_messages).to include("Eメールはすでに存在します")
      end

      it 'passwordが空だと登録できない' do
        @user.password = nil
        @user.valid?
        expect(@user.errors.full_messages).to include("パスワードを入力してください", "パスワード（確認用）とパスワードの入力が一致しません")
      end

      # deviseはdefaultでpasswordに6文字以上出ないと登録できないというバリデーションが設定されています
      it 'passwordが6文字以下だと登録できない' do
        # passwordを5文字に設定
        @user.password = "12345"
        @user.password_confirmation = "12345"
        @user.valid?
        expect(@user.errors.full_messages).to include("パスワードは6文字以上で入力してください")
      end:tired_face:

      it 'passwordが正しくてもpassword_confirmationが一致しないと登録できない' do
        # # こちらはnilにするとテストにパスしなくなりますので""でないといけません
        @user.password_confirmation = ""
        @user.valid?
        expect(@user.errors.full_messages).to include("パスワード（確認用）とパスワードの入力が一致しません")
      end
    end
  end
end
```

個人的に最初全く思いつかなかったところがemailアドレスが他のユーザーと同じだと登録できないというところです。
ポイントは最初に2人のユーザーを作成し１人目のemailアドレスを2人目のemailアドレスとして同じ値に更新し、意図的に同じにすることでした。

もうひとつポイントは最後の'passwordが正しくてもpassword_confirmationが一致しないと登録できない'の部分です。
こちらパスワードconfirmationをnilにすると上手くいきませんので" "で空であることを明示的にする必要があります。
逆に他のnameやemailなどは" "としても問題ありません。

以上が今回実装したUserモデルの単体テストコードの内容になります。
出来るだけ詳細に書こうと思い随分長くなってしまいましたが次回はよりわかりやすく書けるようにしていきたいと思います。
次は結合テストーコードについて書けたらなーと思っています。
間違っているところや分かり辛い箇所があればご指摘いただけると有り難いです。

参考 => [FactoryBot Github](https://github.com/thoughtbot/factory_bot)
参考 => [RSpec Github](https://github.com/rspec/rspec-rails)
参考 => [Faker Github](https://github.com/faker-ruby/faker)