[Rails] RSpecでテストをしてみる

##はじめに

今まで難しそうと思って避けてきたテスト。
そろそろサービスを本格的に動かそうと思ったためしっかりとテストというものを理解するための第一歩として学んだことをこちらの記事にかきます。
いわゆる備忘録ですね。
自分自身が初心者なので初心者の視点で書いていきます。
もっとこういう書き方や手法があるなどあればコメントいただけてら嬉しいです！

##RSpecの導入

おきまりの導入をしていきます。
まあ、ここは特に何も考えずに

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

group :development do
  gem 'web-console' #もともと記載されている可能性があります。
end


```
Gemfileを編集したら、おきまりのbundle installします。

##RSpecの基本設定

まずはRSpec用の設定ファイルを作成する必要があります。
ターミナルで

```
$ rails g rspec:install
```
すると以下のファイルが生成されます。

```
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb
```
生成されたファイルのうち***.rspec***ファイルに下記を追記します。

```:.rspec
--format documentation
```
上記を追記するとテスト結果を見やすく出力してくれます。
後ほど出てくるcontextとitを段階的に書いてくれるものだと考えればいいと思います。
まあ、なくても問題ないものだと思います。
ちなみに上記以外のオプションを追加するとテキスト出力や、並行表示とかもできるので持っときあめたい方は調べてみてください。

とりあえずここまでで導入は完了です。
ここからテストコードを書いていく作業です。


##テストを実行してみる
RSpecはターミナルでコマンドを打つことにより実行が可能です。
まだ、テストコードは何も書いていませんが、実際に動くかどうか確かめるため下記コードをターミナルに打ち込みます。

```:ターミナル
$ bundle exec rspec
```
上記がテストを実行する際のコマンドです。
結果下記のような表示になればオッケーです。

```:ターミナル
No examples found.


Finished in 0.00031 seconds (files took 0.19956 seconds to load)
0 examples, 0 failures
```

ここからモデルやコントローラー、ルート、ビューなどのテスト用のファイルを作成し、テストコードを書いていきます。

##単体テスト（モデル）
単体テストであるモデルのバリデーションに関するテストコードを書いて実行することを目標とします。

####事前に知っておくべきこと
テストコードはspecファイルを作成し記述していきます。
一つのファイルを作って書いていくのではなく、わかりやすいようにモデルに関するテスト用ファイルであればspec/models/以下に、コントローラーに関するテスト用ファイルであればspec/controllers/以下にspecファイルを配置します。

***specファイルの命名規則***
specファイルは対応するクラス名_spec.rbという名前になります。
今回はお問い合わせフォーム(contactモデル / contact.rb)のバリデーションに関してのテストを行うため、その場合の名前は「contact_spec.rb」になります。


###テストコードの基本
テストコードを書くファイルに関してわかったところで、実際に書くまえに超超超基本なテストコードを書いておきます。
***「1 + 1が2になることを確かめる」という簡単なテストコード***

```sample_spec.rb
describe "hogehoge" do
  it "1 + 1は2になること" do
    expect(1 + 1).to eq 2
  end
end

```

語彙力は無視して自分的解説をします。

```sample_spec.rb

describe "hogehoge" do        #describe(説明するという意)でこのテストのグループを作ります。
  it "1 + 1は2になること" do     #it "~~~" do の ~~~部分にこのコードがなんのテス等をしているか書きます
    expect(1 + 1).to eq 2     #この部分が実際にテストが成功するかどうかチェックされる式となります。
  end
end

```
上記の　expect(1 + 1).to eq 2 の部分の式のことをエクスペクテーションと言います。
●　expect(X).to eq Y　
こんな感じでxの部分に入れた式の値がYの部分の値と等しければ、テストが成功します。
また、eqの部分を、マッチャと言います。

マッチャとは、エクスペクテーションの中で、テストが成功する条件を示します。
例えば上記のようにeqは「等しければ」という意味になります。
他にもinclude(含んでいれば)、valid(バリデーションされれば)など複数のマッチャが存在します。
たくさんあるので書きたいテストに合わせて利用してください。

##実際にモデルのバリデーションのテストを行う。

今回使用するcontactモデル（お問い合わせ機能）は下記のようになっております。

```contact.rb

class Contact < ApplicationRecord

#送信者の名前
  validates :name, presence: true 

#お問い合わせに対して返信先のメアド
  validates :email, presence: true, length: {maximum:255},
                                    format: {with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i}

#お問い合わせの件名
  validates :title, presence: true

#お問い合わせ内容
  validates :message, presence: true
end

```

上記のバリデーションに対してテストコードを書いていきます。

#### spec/models/contact_spec.rbファイルを作成する。
作成したcontact_spec.rbを下記のように編集します。

```contact_spec.rb
require 'rails_helper'
  RSpec.describe Contact, type: :model do
    it "nameがない場合は登録できないこと" do
  end
end

```

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

書いたところで一度、bundle exec rspecというコマンドをターミナルで実行します。
現在はまだテストとして評価される式を書いていないので、テストは無条件でパスします。
下記のようになればオッケーです。


ターミナル
```:ターミナル
Contact
  nameがない場合は登録できないこと

Finished in 0.23784 seconds (files took 4.71 seconds to load)
1 example, 0 failures
```
　
####nameが空の場合登録できないことを確かめるテストコードを書く
先ほど書いたテストコードに下記を追記します。

```contact_spec.rb

RSpec.describe Contact, type: :model do
  it "nameがない場合は登録できないこと" do
    contact = Contact.new(name: "", email: "kkk@gmail.com", title: "返品について", message: "注文番号000000000の商品を返品したいです。")
    contact.valid?
    expect(contact.errors[:name]).to include("を入力してください")
  end
end

```
***[3行目]***でテストしたいプロパティを持ったcontactクラスのインスタンスを新規作成します。
「nameが空である場合登録できないこと」を確かめるテストコードを作成したいのでnameの値を空にし、それ以外は適当な値をセットした状態でcontactクラスのインスタンスを作成しています。

***[6行目]***で作成したインスタンスがバリデーションによって保存ができない状態かチェックします。

***[7行目]***でチェックした結果インスタンスが持つエラー文が期待したものであるか確かめます。
contact.errorsに対してハッシュのバリューの取り出し方でカラム名を指定すると、そのカラムが原因のエラー文が入った配列を取り出すことができます。こちらに対して、includeというマッチャを利用してエクスペクテーションを作っています。
今回はバリデーションのエラー文で"を入力してください"が出力されるため、その内容を記述しております。

上記の流れは下記のコンソールでの検証と同じ意味を持ちます。

```:ターミナル

#コンソールを立ち上げる
$ rails c
#nameの値が空であるcontactクラスのインスタンスを作成する
>contact = Contact.new(name: "", email: "kkk@gmail.com", title: "返品について", message: "注文番号000000000の商品を返品したいです。")
#valid?メソッドを利用する
>contact.valid?
#errorsメソッドを利用する
>contact.errors
=> #<ActiveModel::Errors:0x007ffa6ce07ef0
 @base=
  #<Contact:0x007ffa6d3430b8
#中略
 @messages={:name=>["を入力してください"]}>
```


ここでRSpecのコマンドを入力しテストの実行をします。

```:ターミナル
$ bundle exec rspec
```
下記のような結果になれば成功です。

```:ターミナル
Contact
  nameがない場合は登録できないこと

Finished in 0.07346 seconds (files took 2.31 seconds to load)
1 example, 0 failures
```

もしエラーが表示される場合はエラーメッセージがターミナルに表示されるのでよく読めば解決できることが多いです。
また、上記のようにコンソールで一度一連の流れとして確認してみると少し理解が深まる気がします。


##[効率的にするため]factory_botの導入

先ほどはテストコードを一つ書きましたが、name以外のmailのテストを書く際は下記のようになります。

```contact_spec.rb

RSpec.describe Contact, type: :model do
  it "nameがない場合は登録できないこと" do
    contact = Contact.new(name: "", email: "kkk@gmail.com", title: "返品について", message: "注文番号000000000の商品を返品したいです。")
    contact.valid?
    expect(contact.errors[:name]).to include("を入力してください")
  end

  it "emailがない場合は登録できないこと" do
    contact = Contact.new(name: "takashi", email: "", title: "返品について", message: "注文番号000000000の商品を返品したいです。")     
    contact.valid?
    expect(contact.errors[:email]).to include("を入力してください")
  end
end

```
単純にemailの部分をからにしているのですが、インスタンスを毎回作成するとコード量が増えてしまいます。
これを改善するのが***factory_bot***です。

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

factory_botを利用すると下記のような記述となります。

```contact_spec.rb

RSpec.describe Contact, type: :model do
  it "nameがない場合は登録できないこと" do
    contact = build(:contact, name: "")
    contact.valid?
    expect(contact.errors[:name]).to include("を入力してください")
  end

  it "emailがない場合は登録できないこと" do
    contact = build(:contact, email: "")      
    contact.valid?
    expect(contact.errors[:email]).to include("を入力してください")
  end
end

```
あらかじめインスタンスを別ファイルで作成しそれを呼び出しています。

####導入手順
***gem 'factory_bot_rails'***をrspec-railsと同じ環境に追加

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

作成したcontacts.rbファイルを以下のように編集します。

```contacts.rb
FactoryBot.define do
  factory :contact do
    name {"takashi"}
    email {"kkk@gmail.com"}
    title {"返品について"}
    message {"注文番号000000000の商品を返品したいです。"}
  end
end

```

するとインスタンス作成の際下記のような記述をすることで、設定したテンプレートの内容でインスタンスの作成ができます。

```
#factory_botを利用しない場合
contact = Contact.new(name: "", email: "kkk@gmail.com", title: "返品について", message: "注文番号000000000の商品を返品したいです。")
#factory_botを利用する場合
contact = FactoryBot.build(:contact)
```

だいぶスッキリました。
また、spec/rails_helper.rbを以下のように編集することでレシーバーであるクラスのFactoryBotという記述を省略することができます。

```rails_helper.rb
RSpec.configure do |config|
  #下記の記述を追加
  config.include FactoryBot::Syntax::Methods

  #省略
end
```

上記踏まえて追加でテストコードを書いていきます。

```contact_spec.rb

require 'rails_helper'

RSpec.describe Contact, type: :model do
  it "nameがない場合は登録できないこと" do
    contact = build(:contact, name: "")
    contact.valid?
    expect(contact.errors[:name]).to include("を入力してください")
  end

  it "emailがない場合は登録できないこと" do
    contact = build(:contact, email: "")      
    contact.valid?
    expect(contact.errors[:email]).to include("を入力してください")
  end

  it "@マークのあとのドメイン" do
    contact = build(:contact, email: "example@eee")
    contact.valid?
    expect(contact.errors[:email]).to include("は不正な値です")
  end

  it "titleがない場合は登録できないこと" do
    contact = build(:contact, title: "")      
    contact.valid?
    expect(contact.errors[:title]).to include("を入力してください")
  end

  it "messageがない場合は登録できないこと" do
    contact = build(:contact, message: "")      
    contact.valid?
    expect(contact.errors[:message]).to include("を入力してください")
  end
end


```

こちらでとりあえず完成形です。

##終わりに

今回はcontactモデルのバリデーションに関してテストを実行しましたが、WEBアプリの仕様によって様々な書き方があります。
テストという世界を少しでも理解していただくことに役立てば幸いです。
コントローラーのテストの仕方はまた違ったやり方になるっぽいのでそのうちアップできればなと思います。

自分も少しテストを理解してきた中で、次のアプリケーションを開発するときはテストコードを書いてから実装してみたりしたいなと考えるようになりました。
記事の内容に関して修正や訂正点があればコメントいただけると幸いです。

長々とお読みいただきありがとうございました！