Rails書評アプリ　RSpec導入編

#RSpec導入
Railsの標準で入っているminitestではなく、実務に置いてminitestより採用率の高いRSpecを導入する。
テストの手法である**TDD(Test Driven Development),BDD(Behavior Driven Development)**に関しては追々学んでいく。
##各種設定
Capybara・・・webアプリケーションのブラウザ操作をシミュレーションできる
FactoryBot・・・テスト用のデータベースの作成をサポートするgem
###各Gemインストール

```Gemfile
group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails',       '~> 4.0.0'
  gem 'factory_bot_rails', '~> 4.11'
  gem 'capybara',          '~> 2.13'
end
```
`$bundle`
`$bin/rails g rspec:install`
minitestのディレクトリを削除
`$rm -r ./test`
testの実行結果が見やすいように以下の記述を.rspec内に書いておく
`--format documentation`
色付け
`--color`
###Capybarの初期準備

```spec/spec_helper.rb
require 'capybara/rspec'

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
```
###webdriversのセッティング
ChromeDriverをかんたんに導入してくれるwebdriversをインストールする
`gem 'webdrivers'`　追記
`$ mkdir spec/support`
`$ touch spec/support/driver_setting.rb`

```spec/support/driver_setting.rb
RSpec.configure do |config|
  config.before(:each, type: :system) do
    # Spec実行時、ブラウザが自動で立ち上がり挙動を確認できる
    # driven_by(:selenium_chrome)

    # Spec実行時、ブラウザOFF
    driven_by(:selenium_chrome_headless)
  end
end
```
###FactoryBotでテストデータを作成できるように準備する
通常FactoryBotをつけないと、メソッドを呼べない
user = FactoryBot.create(:user)

上の設定を追加することで、FactoryBotの記述が省略できる。
user = create(:user)

```spec/rails_helper.rb
require "support/factory_bot"
```

```spec/support/factory_bot.rb
RSpec.configure do |config|
  # FactoryBotの呼び出し簡略化
  config.include FactoryBot::Syntax::Methods

  # springが原因でfactoryが正しく読み込まれないことを防ぐ
  config.before :all do
    FactoryBot.reload
  end
end
```

```spec/rails_helper.rb
# このコードのコメントアウトを外す
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
```
**Userのファクトリを定義する**
*「ファクトリ(Factory)」とはデータを作成することを簡単にする仕組みのこと*

```spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'テストユーザー' }
    email { 'test1@example.com' }
    password { 'password' }  
  end
end
```
**Reviewのファクトリを定義する**

```spec/factories/reviews.rb
FactoryBot.define do
  factory :review do
    title { "MyString" }
    author { "MyString" }
    description { "MyText" }
    user
  end
end
```
###spring-commands-rspecを導入する
このgemによってテストの起動時間が早くなる

```Gemfile
group :development do
  gem 'spring-commands-rspec'
end
```
`bundle install`し、次に`bundle exec spring binstub rspec`をするとbinディレクトリにrspecファイルが生成される

```bin/rspec
#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
require 'bundler/setup'
load Gem.bin_path('rspec-core', 'rspec')
```

###Shoulda Matchersの設定
Shoulda Mathersとは、RSpecやMinitestにワンライナーのテストのシンタックスを追加することで、簡潔かつ直感的にテストを書けるようになるgem

```Gemfile
group :test do
  gem 'shoulda-matchers'

  # rails5以降の場合、 
  gem 'shoulda-matchers', git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5'
end
```

```spec/support/shoulda_matchers.rb
Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end
```

```spec/rails_helper.rb
require "support/shoulda_matchers"
```
```config/application.rb
require_relative 'boot'
require 'rails/all'
require 'uri'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module BookReviewApp
  class Application < Rails::Application
    config.load_defaults 5.1
    config.generators.template_engine = :slim
    config.i18n.default_locale = :ja
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]

    config.generators do |g| 
      g.test_framework :rspec,
        fixtures: false, 
        view_specs: false, 
        helper_specs: false, 
        routing_specs: false
    end
  end
end
```

##モデルテストのコードを書いてみる
specフォルダの中にmodelsというフォルダを作成して、modelsフォルダの中にuser_spec.rbファイルを作って以下のコードを書く
`$ mkdir spec/models`
`$ touch spec/models/user_spec.rb`

```spec/mogels/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"

  context "if name is entered" do              
    it 'if user registration is successful' do 
    end
  end

  context "if name is not entered" do          
    it 'if user registration fails' do
    end
  end
  
  context 'if email is entered' do
    it 'if user registration is successful' do
    end
  end

  context 'if email is not entered' do
    it 'if user registration fails' do
    end
  end

  context "if password is entered" do
    it 'if user registration is successful' do
    end
  end

  context "if password is not entered" do
    it 'if user registration fails' do
    end
  end
end
```

```
 # テストの説明文の言語は構文によって変える

　# example構文
example '日本語' do
  # コードを記述する
end

  # it構文
it 'English' do
  # コードを記述する
end
```

このファイルだけをテストしたい場合、ファイルを指定する必要がある
`$ bundle exec rspec spec/models/user_spec.rb`

examples：存在するテスト  /  failures：失敗したテスト  /  pending：実行されなかったテスト

##レビューの一覧表示機能のSystem Spec
`$ mkdir spec/system/`
`$ touch spec/system/reviews_spec.rb`
###まずはコメントでSpecの大枠を作る

```spec/system/reviews_spec.rb
require 'rails_helper'

describe 'レビュー管理機能', type: :system do
    describe '一覧表示機能' do
        before do 
        # ユーザーAを作成しておく
        # 作者がユーザーAであるレビューを作成しておく
        end

context 'ユーザーがログインしているとき' do
    before do 
        # ユーザーAがログインする
    end

    it 'ユーザーAが作成したレビューが表示される' do
        # 作成済みのレビューのタイトルが画面上に表示されていることを確認
    end
  end
 end
end
```
`describe`には、「何について仕様を記述しようとしているのか」(テストの対象)を記述する。
一番外側のdescribeには、そのSpecファイル全体の主題を記述する。
階層の深いdescribeにはより細かいテーマを記述する。

`context`は、テスト内容を「状況・状態」のバリエーションごとに分類するために利用する。
様々な条件で正しく動くか試す必要があるテストを整理して見やすくすることができる。

`before`は、その領域全体の「前提条件」を実行するためのコードを記述する場所。
describeやcontext内にbeforeを記述すると、対応するdescribeやcontextの領域内のテストコードを実行する前に、beforeのブロック内に書かれたコードを実行してくれる。

`it`は、期待する動作を文章と、ブロック内のコードで記述する。itの中に書いた期待する動作通りに対象が動作すれば、テストは成功となる。

---

※以下の順番でコードが実行される
**最初のbefore**
・ユーザーAを作成しておく(テストデータの準備)
・作成者がユーザーAであるレビューを作成しておく(テストデータの準備)
**2つめのbefore**
・ユーザーAでブラウザからログインする(前提となっているユーザーの操作をしておく)
**it**
・ユーザーAの作成したレビューのタイトルが画面上に表示されていることを確認

---

###1つ1つのコメントを、実際のコードに置き換えていく

```spec/system/reviews_spec.rb
require 'rails_helper'

describe 'レビュー管理機能', type: :system do
    describe '一覧表示機能' do
        before do 
        # ユーザーAを作成しておく
        user_a = create(:user, name: 'ユーザーA', email: 'a@example.com')
        # 作者がユーザーAであるレビューを作成しておく
        create(:review, name: '最初のレビュー', user: user_a)
        end

context 'ユーザーがログインしているとき' do
    before do 
        # ユーザーAがログインする
        visit login_path
        fill_in 'メールアドレス', with: 'a@example.com'
        fill_in 'パスワード', with: 'password'
        click_button 'ログインする'
    end 

    it 'ユーザーAが作成したレビューが表示される' do
        # 作成済みのレビューのタイトルが画面上に表示されていることを確認
        expect(page).to have_content '最初のレビュー'
    end
  end
 end
end
```
```
 # テストが失敗した場合
$ bundle exec rspec spec/system/reviews_spec.rb

レビュー管理機能
  一覧表示機能
    ユーザーがログインしているとき
      ユーザーAが作成したレビューが表示される (FAILED - 1)

Failures:

  1) レビュー管理機能 一覧表示機能 ユーザーがログインしているとき ユーザーAが作成したレビューが表示される
     Failure/Error: user_a = FactoryBot.create(user, name: 'ユーザーA', email: 'a@example.com')
     
     NameError:
       undefined local variable or method `user' for #<RSpec::ExampleGroups::Nested::Nested::Nested:0x00007fed26346f28>
     
     [Screenshot]: tmp/screenshots/failures_r_spec_example_groups_nested_nested_nested_ユーザーaが作成したレビューが表示される_412.png


     
     # ./spec/system/reviews_spec.rb:7:in `block (3 levels) in <top (required)>'

Finished in 4.19 seconds (files took 5.32 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/system/reviews_spec.rb:21 # レビュー管理機能 一覧表示機能 ユーザーがログインしているとき ユーザーAが作成したレビューが表示される
```
##レビューの新規作成機能のSystem Spec

```spec/system/reviews_spec.rb
.
.
.
describe 'レビュー新規作成機能' do
    let(:login_user) { :user_a }

    before do 
        visit new_review_path
        fill_in 'タイトル', with: review_name
        click_button '投稿する'
    end

    context '新規投稿画面でタイトルを入力したとき' do
        let(:review_name) { '新規投稿のテストを書く' }

        it '正常に投稿される' do
            expect(page).to have_selector 'alert-success', text: '新規投稿のテストを書く'
        end
    end

    context '新規投稿画面でタイトルを入力しなかったとき' do
        let(:review_name) { '' }

        it 'エラーになる' do
            within '#error_explanation' do
            expect(page).to have_content 'タイトルを入力してください'
        end
      end
    end
  end
end
```
letは「before処理でテストスコープの変数に値を代入する」のに近いイメージで利用できる機能(実際に変数を作るわけではない)
`let(定義名){ 定義の内容 }`

have_selectorというマッチャを使うと、HTML内の特定の要素をセレクタ(CSSセレクタ)で指定することができる
`have_selector`

withinというメソッドを使うと、withinのブロックの中でpageの内容を検査することで、探索する範囲を画面内の特定の範囲に狭めることができる
`within '範囲を指定' do`


##Userモデルのテスト
コメントで大枠を作る

```spec/models/user_spec.rb
 # validations
 describe "validations" do
   # 存在性
   describe "presence" do
     # 名前、メールアドレス
     it "name and email should not ro be empty/falsy"
     # パスワード、パスワード確認
     context "when password and confirmation is not present" do
       it "@user is invalid"
     end
    end
   # 文字数 charesters
   describe "charesters" do
     # 名前： 最大 20 文字
     context "when name is too long"
       it "@user is inavlid"
     # パスワード、パスワード確認： 最小 6 文字
     describe "when password is too short"
       it "@user is inavlid"
   end
 # email のフォーマット
 describe "email format" do
   # invalid なフォーマット
   context "when invalid format" do
     it "@user is inavlid"
   # valid なフォーマット
   context "when valid format" do
     it "@user is valid"
   end
   end
  end
end
```
コメントを埋めていく

```spec/models/user_spec.rb
RSpec.describe User, type: :model do
  # 保留されたテンプレート
  pending "add some examples to (or delete) #{__FILE__}"

 # validations
 describe "validations" do
   # 存在性
   describe "presence" do
     # 名前、メールアドレス
     it { should validate_presence_of :name }
     it { should validate_presence_of :email }
     # パスワード、パスワード確認
     context "when password and confirmation is not present" do
       before { user.password = user.password_confirmation = " " } 
       it { should_not be_valid }
     end
    end
   # 文字数 charesters
   describe "charesters" do
     # 名前： 最大 20 文字, パスワード： 最小 6 文字
     it { should validate_length_of(:name).is_at_most(20) }
     it { should validate_length_of(:password).is_at_least(6) }
   end
 # email のフォーマット
 describe "email format" do
   # invalid なフォーマット
   context "when invalid format" do
     # 無効なオブジェクト
     it "should be invalid" do
       invalid_addr = %w[user@foo,com user_at_foo.org example.user@foo. foo@bar_baz.com foo@bar+baz.com]
       invalid_addr.each do |addr|
         user.email = addr
         expect(user).not_to be_valid
       end
      end
    end
  end
   # valid なフォーマット
   context "when valid format" do
     # 有効なオブジェクト
     it "should be valid" do
       valid_addr = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
       valid_addr.each do |addr|
         user.email = addr
         expect(user).to be_valid
       end
     end
   end
  end
end
```
↓テスト結果↓

```console.log
$ bundle exec rspec spec/models/user_spec.rb

User
  add some examples to (or delete) /Users/kiyokawakouji/Rails-app/book_review_app/spec/models/user_spec.rb (PENDING: Not yet implemented)
  validations
    presence
      is expected to validate that :name cannot be empty/falsy
      is expected to validate that :email cannot be empty/falsy
      when password and confirmation is not present
        example at ./spec/models/user_spec.rb:17 (FAILED - 1)
    charesters
      is expected to validate that the length of :name is at most 20
      is expected to validate that the length of :password is at least 6
    email format
      when invalid format
        should be invalid (FAILED - 2)
    when valid format
      should be valid (FAILED - 3)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User add some examples to (or delete) /Users/kiyokawakouji/Rails-app/book_review_app/spec/models/user_spec.rb
     # Not yet implemented
     # ./spec/models/user_spec.rb:5


Failures:

  1) User validations presence when password and confirmation is not present 
     Failure/Error: before { user.password = user.password_confirmation = " " }
     
     NameError:
       undefined local variable or method `user' for #<RSpec::ExampleGroups::User::Validations::Presence::WhenPasswordAndConfirmationIsNotPresent:0x00007fd1586e2770>
     # ./spec/models/user_spec.rb:16:in `block (5 levels) in <top (required)>'

  2) User validations email format when invalid format should be invalid
     Failure/Error: user.email = addr
     
     NameError:
       undefined local variable or method `user' for #<RSpec::ExampleGroups::User::Validations::EmailFormat::WhenInvalidFormat:0x00007fd15831d6f8>
     # ./spec/models/user_spec.rb:34:in `block (6 levels) in <top (required)>'
     # ./spec/models/user_spec.rb:33:in `each'
     # ./spec/models/user_spec.rb:33:in `block (5 levels) in <top (required)>'

  3) User validations when valid format should be valid
     Failure/Error: user.email = addr
     
     NameError:
       undefined local variable or method `user' for #<RSpec::ExampleGroups::User::Validations::WhenValidFormat:0x00007fd1582d5790>
     # ./spec/models/user_spec.rb:46:in `block (5 levels) in <top (required)>'
     # ./spec/models/user_spec.rb:45:in `each'
     # ./spec/models/user_spec.rb:45:in `block (4 levels) in <top (required)>'

Finished in 0.61427 seconds (files took 5.94 seconds to load)
8 examples, 3 failures, 1 pending

Failed examples:

rspec ./spec/models/user_spec.rb:17 # User validations presence when password and confirmation is not present 
rspec ./spec/models/user_spec.rb:31 # User validations email format when invalid format should be invalid
rspec ./spec/models/user_spec.rb:43 # User validations when valid format should be valid
```
**以下テスト結果の解説**
`panding`はコメントアウトでも書かれている通り、RSpecの書き方を示しているテンプレート(今回はあえて消さずに起こしておいた)なのでスルー。
`(FAILED - 数字)`がテストコードが失敗したことを示している。今回は３カ所のテストコードが失敗したことになる。
`Failures:~`はテストが失敗した該当コードとエラー内容が出力されている。
`Failures example:~`はテストが失敗したファイルの該当コードの行と期待する動作が出力されている。
今回はモデルのテストなのでvalidatesを確認していく

```app/models/user.rb
class User < ApplicationRecord
 devise :database_authenticatable, :rememberable, 
        :validatable, :timeoutable, :registerable, :confirmable

 # nameの値が空でないか？
 validates :name, presence: true
 # nameの値が20文字以下か？
 validates :name, length: { maximum: 20 }
 # emailアドレスをデータベースに保存する際にメールアドレスを小文字にする
 before_save { self.email = email.downcase }
 VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
 # emailがその確認用の入力と一致しているか？
 validates :email, { presence: true, format: { with: VALID_EMAIL_REGEX } }
 validates :email, :encrypted_password, confirmation: true
 # passwordの値が空でないか？
 validates :password, presence: true
 # passwordの値が6文字以上か？
 validates :password, length: { minimum: 6 }
 # データベース上でUserとReviewを紐づける
 has_many :reviews, dependent: :destroy, foreign_key: :review_user_id
end

```

#番外編
**失敗したテストだけを再実行できる(--only-failuresオプション)**の設定

```spec_helper.rb
RSpec.configure do |config|
  config.example_status_persistence_file_path = "./spec/examples.txt"
end

------------------------------------------------------------------

gitを使っている場合は .gitignore に以下の設定も追記します。
spec/examples.txt
```
`$ bundle exec rspec --only-failures`
##rspecのオプション

```
rspecのオプション一覧表示
$ rspec --help

テストの一覧を実行せずに出力
$ rspec --dry-run

テストの警告を出力
$ rspec --warnings
このコマンドはspecファイル中の警告を表示する。スペルミスなどがあればそれも警告の内に入れ、出力する
```
試しに`$ rsoec --warnings`をした際に以下の出力がされた

```
WARN: Unresolved or ambiguous specs during Gem::Specification.reset:
      diff-lcs (>= 1.2.0, < 2.0)
      Available/installed versions of this gem:
      - 1.4
      - 1.3
WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'
Please report a bug if this causes problems.
~~省略~~
```
同一のgemが複数のバージョンでインストールされていて、システムでどれを扱うか決められないと言われている。
旧バージョンのgemを削除して、一つに絞るか、具体的なバージョンを決める必要がある。

##テストの種類
**全体的なテスト**
・システムテスト(System Spec/Feature Spec)
・統合テスト(Request Spec)
・機能テスト(Controller Spec)
**個々の部品テスト**
・モデル(Model Spec)
・ルーティング(Routing Spec)
・ビュー(View Spec)
・ヘルパー(Helper Spec)
・メーラー(Mailer Spec)
・ジョブ(job Spec)
##テストコード自体の信頼性
テストコードが意図した通りに動いていることを確認しなければならない。これは「テスト対象のコードでいろいろ試すアプローチ（exercising the code under test）」としても知られている。
誤判定ではないことを証明するためには例えば、`to`を`to_not`に変えてエクスペクテーションを反転させる。

```spec/models/user_spec.rb
 # 名がなければ無効な状態であること(成功)
 it "is invalid without a first name" do
   user = User.new(first_name: nil)
   user.valid?
   expect(user.errors[:first_name]).to include("can't be blank")
 end

 # 名がなければ無効な状態であること(失敗)
 it "is invalid without a first name" do
   user = User.new(first_name: nil)
   user.valid?
   expect(user.errors[:first_name]).to_not include("can't be blank")
 end
```
#参考
[Rails RSpecの準備とテストコード基礎](https://qiita.com/bomber0522/items/2d76f81a980e1cc14be3)
[【Rails】『RSpec + FactoryBot + Capybara + Webdrivers』の導入＆初期設定からテストの書き方まで](http://vdeep.net/rubyonrails-rspec-factorybot-capybara)
[Rails + Selenium + DockerでSystemSpecの環境構築](https://qiita.com/ngron/items/f61b8635b4d67f666d75)
[https://github.com/everydayrails/everydayrails-rspec-2017](https://github.com/everydayrails/everydayrails-rspec-2017)
[使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」](https://qiita.com/jnchito/items/42193d066bd61c740612)
[RSpec 設定](https://qiita.com/Atelier-Mirai/items/89b8b38e897d64847c9a)
[Rspecの便利なオプション](https://qiita.com/s_yasunaga/items/cc7489ee4e51ca196c43)
[苦しめられてやっと理解できたRailsコールバックの使い方](https://tech.kitchhike.com/entry/2018/06/30/232400)