【Rails】RSpecでのモデルクラス単体テスト

#はじめに
学習中の備忘録です。

#概要
* 単体テスト準備
* モデルクラスのテストコードを書く
* テストコードを追記する
* テストコードを書く際の原則
* まとめ


#前提
rails5.2.3 RSpec factory_bot
#単体テスト準備
##RSpecのインストール

Gemファイルに追記
また web_console というgemはtest環境で動かすと不具合が起きる可能性があるgemなのでdevelopment環境でのみ動くようにします。

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

group :development do
  gem 'web-console'
end
```
```:ターミナル
% bundle install
```

##RSpecの設定

```:ターミナル
% rails g rspec:install
#RSpec用設定ファイルの作成
```

作成されるファイル

```
Running via Spring preloader in process 1747
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb
```
##事前知識

###RSpec用ディレクトリの構造
RSpecによるテストコードが書かれたファイルのことを、specファイルと呼びます。全てのspecファイルは、先ほどのrails g rspec:installコマンドで生成された「specディレクトリ」の中に格納しておきます。
モデルに関するテスト用ファイルであればspec/models/以下に、コントローラーに関するテスト用ファイルであればspec/controllers/以下に格納されます。appディレクトリ以下にあるテストの対象となるコードの在り処と対応させます。

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

###rails_helper.rb
RailsにおいてRSpecを利用する際に、共通の設定を書いておくファイルです。各テスト用ファイルでこちらのファイルを読み込むことで、共通の設定や、メソッドを適用します。

###spec_helper.rb
rails_helper.rbと同じくRSpec用の共通の設定を書いておくファイルですが、こちらはRSpecをRails無しで利用する際に利用します。



##.rspecに以下を追加
```ruby:.rspec
--require spec_helper
#追加
--format documentation
```

##factory_botのインストール
簡単にダミーのインスタンスを作成することができるGemです。他のファイルで予め各クラスのインスタンスに定めるプロパティを設定しておき、specファイルからメソッドを利用してその通りのインスタンスを作成します。

```ruby:Gemfile
group :development, :test do
  #省略
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end
```
```:ターミナル
% bundle install
```
続いて、specディレクトリ直下に「factories」というディレクトリを作成します。その中に、作成したインスタンスの複数形のファイル名でRubyのファイルを作成します。今回の場合は、users.rbですね。

###users.rbを編集

```ruby:users.rb
FactoryBot.define do

  factory :user do
    name                  {"abe"}
    email                 {"kkk@gmail.com"}
    password              {"00000000"}
    password_confirmation {"00000000"}
  end

end
```
###rails_helper.rbを編集
```ruby:rails_helper.rb
#省略
RSpec.configure do |config|
  #下記の記述を追加
  config.include FactoryBot::Syntax::Methods

  #省略

end
```
以上で準備完了です。

#モデルクラスのテストコードを書く
ここではUserモデルのテストコードを書きます。specディレクトリ直下にmodelsディレクトリを作成。modelディレクトリ直下にuser_spec.rbを作成します。

##user_spec.rbの編集
ここから「nameが空の場合登録できないことを確かめる」テストコードを書いていきます。

```ruby:user_spec.rb
require 'rails_helper'

describe User do
  describe '#create' do

    it "nameがない場合は登録できないこと" do
      user = build(:user, nickname: nil)
      user.valid?
      expect(user.errors[:nickname]).to include("can't be blank")
    end

  end
end
```
**describe**
1行目のdescribeは、直後のdo ~ endまでのテストのまとまりを作ります。describeの後に続く""の中にはそのまとまりの説明を書きます。

**itとexample**
2行目のitはexampleと呼ばれる実際に動作するテストコードのまとまりを表します。itの後に続く""の中にはそのexampleの説明を書きます。

**エクスペクテーション**
実際に評価される式のことです。it do ~ endの間に書きます。上記の式ではexpect(1 + 1).to eq 2の部分がエクスペクテーションです。

**expect(X).to eq Y**
エクスペクテーションの文法です。xの部分に入れた式の値がYの部分の値と等しければ、テストが成功します。eqの部分を、マッチャと言います。

**マッチャ**
エクスペクテーションの中で、テストが成功する条件を示します。例えばeqは「等しければ」という意味になります。他にも include(含んでいれば)、valid(バリデーションされれば)など複数のマッチャが存在します。これらに関しては利用する時に再度説明します。


【1行目】require 'rails_helper'は、rails_helper.rb内の記述を読み込むことで共通の設定を有効にしています。この1行目の記述は、全てのspecファイルに書き込みます。

【2,3行目】連続してdescribeが登場しています。describeは、このようにネスト(入れ子状)にすることができます。ここでは「Userクラスにあるcreateメソッドをテストするまとまり」であることを示しています。このように、describeとdoの間にメソッド名を書く際は#をつけるのが慣習です。

【5行目】テストしたいプロパティを持ったuserクラスのインスタンスを新規作成する
スペックファイルの中では、そのRailsプロジェクトで作成しているモデルクラスを利用することができます。今回は「nameが空である場合登録できないこと」を確かめるテストコードを作成したいのでnameの値を空にし、それ以外はfactories/users.rbでセットした値が自動で入ります。

【6行目】作成したインスタンスがバリデーションによって保存ができない状態かチェックする
続いて、新規作成したuserクラスのインスタンスがバリデーションに引っかかるかどうかを確かめるvalid?メソッドを利用します。

【7行目】チェックした結果インスタンスが持つエラー文が期待したものであるか確かめる。
expectの引数に関して、user.errorsに対してハッシュのバリューの取り出し方でカラム名を指定すると、そのカラムが原因のエラー文が入った配列を取り出すことができます。こちらに対して、includeというマッチャを利用してエクスペクテーションを作っています。


**valid?メソッド**
valid?メソッドを利用すると、ActiveRecord::Baseを継承しているクラスのインスタンスを保存する際に「バリデーションにより保存ができない状態であるか」を確かめることができます。

**errorsメソッド**
valid？メソッドの返り値はtrue/falseですが、valid?メソッドを利用したインスタンスに対してerrorsメソッドを利用すると、バリデーションにより保存ができない状態である場合なぜできないのかを確認することができます。

**includeマッチャ**
includeマッチャは、引数にとった値がexpectの引数である配列に含まれているかをチェックすることができるマッチャです。
今回の場合、「nameが空の場合はcan't be blankというエラーが出るはずだ」ということがわかっているため、include("can't be blank")のように書くことができます。実際にその通りになればこちらのエクスペクテーションはパスし、このコードは意図した動作をすると保証できます。

#テストの実行
##bundle exec rspecコマンド
RSpecのテストコードを利用したテストを実行するためのコマンドです。

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

テスト完了画面

```
2020-04-19 00:45:18 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.

User
  #create
    nameが存在しなければ登録できないこと

Finished in 0.22598 seconds (files took 4.15 seconds to load)
1 examples, 0 failures
```

エラー画面

```
2020-04-19 01:02:31 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.

User
  #create
    nameが存在しなければ登録できないこと (FAILED - 1)

Failures:
      #省略
```

#テストコードを追記する
name同様email,passwordが空では登録できないこと。name,email,password,password_confirmationが存在すれば登録できること。emailが重複していると登録できないことをテストするテストコードを追記していきます。

**be_validマッチャ**
expectの引数にしたインスタンスが全てのバリデーションをクリアする場合にパスするマッチャです。

**重複したemailが存在する場合登録できないこと**
重複に関するパターンです。先にユーザーを１人登録しておき、その後emailに同じ値を持つ別のユーザーが登録できるかチェックすることで確かめられます。 この時含まれるエラー文は、「has already been taken」です。

##user_spec.rbの編集

```ruby:user_spec.rb
require 'rails_helper'
describe User do
  describe '#create' do
    it "nameが存在しなければ登録できないこと" do
      user = build(:user, name: nil)
      user.valid?
      expect(user.errors[:name]).to include("can't be blank")
    end

    it "emailが存在しなければ登録できないこと" do
      user = build(:user, email: nil)
      user.valid?
      expect(user.errors[:email]).to include("can't be blank")
    end

    it "passwordが存在しなければ登録できないこと" do
      user = build(:user, password: nil)
      user.valid?
      expect(user.errors[:password]).to include("can't be blank")
    end

    it "name、email、passwordとpassword_confirmationが存在すれば登録できること" do
      user = build(:user)
      expect(user).to be_valid
    end

    it "重複したemailが存在する場合登録できないこと" do
      #はじめにユーザーを登録
      user = create(:user)
      #先に登録したユーザーと同じemailの値を持つユーザーのインスタンスを作成
      another_user = build(:user)
      another_user.valid?
      expect(another_user.errors[:email]).to include("has already been taken")
    end
  end
end
```

#テストコードを書く際の原則
**①各exampleで期待する値は1つ**
テストコードにおいては、example(it "exampleの説明" do ~ end のまとまり)ひとつに必ずエクスペクテーション(expext(◯◯).to ~)をひとつ含めます。 ２つ以上含めてしまうと、どちらのエクスペクテーションでエラーが出たのか判別できず、正確なテストができないためです。

**②期待する結果をはっきりわかりやすく記述する**
it "〜" doの"〜"の部分は、期待する結果を書いておく場所です。 明快な書き方をすることで、自身の確認やチームメンバーとの共有、顧客への仕様説明が楽になり、コミュニケーションミスも減ります。

**③起きて欲しいことと起きてほしくないこと両方をテストする**
起きて欲しいことをチェックしたら、起きてほしくない場合にどんな結果が起こるかも想定しその通りになるか確かめましょう。 予期せぬ動作が残るのを防ぐためです。

**④境界値をテストする**
6文字以上でバリデーションに引っかかる、という条件の場合は「5文字までは正常」と「6文字以上ならば異常」を確かめるようにします。こちらも、予期せぬ動作を防ぐためです。

**⑤可読性を考えつつ、適度にDRYにする**
DRYとは「Don't Repeat Yourself」の略で、何度も同じことを記述せず効率的にコードを書こう、という原則を意味します。しかし、テストコードにおいては何よりもわかりやすさを優先しましょう。その結果たとえDRYに添えなくなったとしても、わかりづらくなってテストの見落としが起きるよりはましだからです。


#まとめ
コントローラー単体テストは別記事にて。テストコードもまだまだ足らないので追記するかもです。