Ruby on Rails のテストフレームワーク RSpec 事始め

前提として、Ruby on Rails で Hello World が表示できていること。
Ruby on Rails を始める方法は [Ruby on Rails 事始め](https://qiita.com/tatsurou313/items/a2cad29352a23c722f9f) に記載。

# RSpec とは

RSpec とは Ruby プログラマー向けの BDD(Behaviour-Driven Development) ツールです。
ここでの BDD はテスト駆動開発(Test-Driven Development), ドメイン駆動型設計(Domain Driven Design), 受け入れテスト型設計へのアプローチのことです。

RSpec は Gem パッケージとして提供されています。

RSpec の公式サイトは[こちら](https://github.com/rspec/rspec-rails)。

# RSpec の使い方

## RSpec の導入

まずは RSpec の Gem パッケージをインストールします。
（インストールするバージョンは RSpec の公式サイトを元に適宜修正してください）

```ruby:Gemfile
・・・
group :development, :test do
  gem 'rspec-rails', '~> 3.6'
end
・・・
```

ちなみに、RSpec は test フレームワークなのに、なぜインストールグループに development を追加するかというと、RSpec にはテストファイルを作成する generator があり、それを利用するために default の RAILS_ENV である development にインストールしておくと楽だからです。

```sh:RSpecのGemパッケージインストール
$ bundler install
```

Gem パッケージがインストールされたら、次は Rails ソフトウェアに対して RSpec 用の初期ファイルをインストールする。

```sh:RSpecのインストール
$ rails generate rspec:install
Running via Spring preloader in process 9045
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb
```

## Spring を使った RSpec の導入(任意)

Spring を使って RSpec を実行することで 1 回の実行時間を短縮させることが出来ます。

そのためには `spring-commands-rspec` Gem をインストールし、RSpec の stub ファイルを作成します。

```ruby:Gemfile
・・・
group :development, :test do
  gem 'spring-commands-rspec'
end
・・・
```

```sh:RSpecのstubファイル作成
$ bundle exec spring binstub rspec
```

すると bin/rspec ファイルが作成されるので、 `./bin/rspec` を使って RSpec を実行すると Spring を使うことが出来ます。(bundle install 実行時に stub ファイルのインストール先を変更している場合はパスが異なる場合があります)

ファイルの中身を見ると spring 経由で rspec を使うためのラッパであることが分かります。

```ruby: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')
```

## RSpec の実行

インストールが完了した段階でテストは空だが RSpec が実行できるようになる。
bundler インストールしている場合は、`bundle exec` コマンドで実行できる。

```sh:RSpecの実行方法（全テストの実行：オプション無のデフォルト動作）
$ bundle exec rspec
```

```sh:RSpecのmaintain_test_schemaのマイグレーション
$ rails db:migrate RAILS_ENV=test
```

上記のようにオプションを指定せずに RSpec を実行すると全テストを実行する。
テストの数が少ない場合はテストを実行するためにかかる時間の問題はないが、
特定のテストのみ実行すればよい場合には `-e STRING` オプションを指定すれば任意のテストを実行できる。

-e オプションの STRING にはテスト中の describe で記述した名前を指定する。
describe の書き方は後述する。

# テストの作り方

## テストファイルの命名規則

テストファイルは Ruby で記載する。
RSpec は default で spec ディレクトリ配下のファイルを自動でテストファイルとして認識する。（ファイル名規則： `spec/**/*_spec.rb`）
尚、テストファイルは必ず `require 'rails_helper'` を含める必要がある。（RSpec3以降）

RSpec の generator を使ってファイルを作ってからファイルを編集するのがよいでしょう。

```bash:RSpecのgenerator
$ bin/rails g
  : <snip>

Rspec:
  rspec:controller
  rspec:feature
  rspec:helper
  rspec:install
  rspec:integration
  rspec:job
  rspec:mailer
  rspec:model
  rspec:observer
  rspec:request
  rspec:scaffold
  rspec:view

  : <snip>
```

```bash:例modelテスト用のファイルspec/models/test_spec.rbを作成する場合
$ bin/rails g rspec:model test
      create  spec/models/test_spec.rb
  : <snip>
```

```spec/models/test_spec.rb
require 'rails_helper'

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

## テストファイルの記述ルール

テスト記述ルールとしては、テストする内容を説明する文章を引数として、describe, context, it を使って記述する。
1 つのテスト内容は it で記述し、期待する動作(expect)を 1 つ含める。

```ruby:
require 'rails_helper'

RSpec.describe Diff, type: :model do
  it "is valid with diff string" do
    diff = Diff.new(diff: "some diff")
    expect(diff).to be_valid
  end
end
```

it に続く文字列は動詞から始め、should は使わない方がよいようです。(RSpec v2.11からshouldの代わりにexpectを使うこととなりshouldという表現が使わなくなったようです。[参考情報](https://github.com/rspec/rspec-expectations/blob/master/Should.md))

ここまでで 1　つのテスト項目の記述ですが、実行するとなぜ it に続いて動詞から始まる説明文を記述していたのか理由が分かります。(spec/spec_helper.rbでconfig.default_formatter = "doc"を設定した状態での結果)

```sh:rspec実行結果
Diff
  is valid with diff string

Finished in 0.02296 seconds (files took 0.35495 seconds to load)
1 example, 0 failures
```

このように、describe, it で記述した内容がテスト結果に表示されるため、実行結果を見れば何をテストしているのかが分かるようになっています。

また、テストが特定の条件を想定する場合は context を使ってその条件を記述します。
ここで describe, context は入れ替えても動作しますし、以下の例では describe, context は記述せず it だけ記述しても動作しますが、テストが想定する内容が分かりやすくなり、将来的に after/before を使ってテスト用データを用意する場合等に、コードのスコープが明確になるメリットもあるため積極的に使うとよいでしょう。

```ruby:
require 'rails_helper'

RSpec.describe Micropost, type: :model do
  describe "search posts by term" do
    context "when no post is found" do
      it "returns an empty collection" do
        expect(Micropost.search("John Doe")).to be_empty
      end
    end
  end
end
```

Spec ファイルについて詳細な情報は[こちら](https://relishapp.com/rspec/)です。

## テストする対象

テストする対象は、モデル、コントローラ、ビュー、ルーティングがあげられる。
RSpec ドキュメントには次のテスト対象が記述されている。

- Model specs
    - モデルの validation 等をテストします
- Controller specs
    - テストは RSpec.describe `${ControllerClass}` do ～ end を使って記述する
        - オプションとして `type: :feature` を指定する
    - Rails5 からは[Request specsを使うことが推奨](http://rspec.info/ja/blog/2016/07/rspec-3-5-has-been-released/)されている)
- Request specs
    - 統合テストの細かいラッパーを記述します
        - 基本的にスタブは使わない(作り方次第)
        - ルーティングとコントローラの双方の動作を記述する
    - テスト内容の例
        - 1回の request を指定する
        - 複数の requests を複数の controllers にまたがって指定する
        - 複数の requests を複数の sessions にまたがって指定する
- Feature specs
    - テストは RSpec.feature `${Some feature test name}` do ～ end を使って記述する
        - オプションとして `type: :feature` を指定する
    - Feature テストとは高レベルのアプリケーションの挙動について行うテストのこと。
    - Feature テストはブラウザ操作（ボタンクリックや input ボックスへの入力）を行う。
        - ブラウザ操作を行うために capybara （Gem パッケージ）をインストールする必要がある。
        - capybara がインストールされていないと rspec 実行時にテストが pending される。
- View specs
    - ビューに特定の文字列が含まれること等をテストします
- Helper specs
    - ヘルパーメソッドを実行して、意図した結果が返ってくることをテストします
- Mailer specs
- Routing specs
    - パスが意図したコントローラへルーティング出来ることをテストします
- Job specs
- System specs

その他、サービス層としてクラスを定義している場合等、spec ディレクトリ配下に spec/services のようにディレクトリを作成してテストを作成することが出来ます。

その場合、`type: :service` のように開発チーム内で一定のルールを設けて指定するようにしましょう。
(特定の spec に対して設定やテスト前の初期化を行う場合に type が統一されていると都合がよいため)


## テストファイルに記述する内容

テストする対象が決まったら、テストする項目を記述していく。
このときのポイントとして、テストファイル内には 「＜テストする対象＞は＜～＞であることが正しい」といった言葉に置き換えて項目を列挙する。

例えば「User は name, emailAddress を所有していることが正である」場合、

```rb:spec/models/user_spec.rb
describe "User" do                    # User モデルについて記述（describe）する
  it "is valid with a name and email" # name と email を保持していることが正である
  it "is invalid without a name"      # name が無いと無効である
end
```

などと、テスト項目（it 以降に記述した内容）を記述していく。
（なお、未だテスト項目の名前のみでテスト自体は記述していない）

テスト項目には確認したい**ただ1つ**の内容を記述する。
また、describe と it の内容を繋げて読むと文章になるよう、it 文では動詞から始める。
（上記例では "User is valid with a name and email." が文章になる）

参考情報：https://leanpub.com/everydayrailsrspec-jp/read

### controller テストについて

get, post, patch, delete メソッドを実行するとテスト対象となるコントローラに対してオプションで指定したアクションが実行される。
これに応じて response.status が 200 であることなどをテスト出来る。

尚、テスト対象と異なるコントローラのアクションを呼び出したい場合は `redirect_to` を使えばよい。

例: セッションを保存するSessionsControllerがあり、ログイン前にアクセスしたURLを記録する動作をテストする場合

```spec/controllers/sessions_controller_spec.rb
require 'rails_helper'

RSpec.describe SessionsController, type: :controller do
  include LoginHelper

  let(:user) { FactoryBot.create(:user, name: 'michael') }

  it "get new" do
    get :new
    expect(response.status).to eq(200)
  end

  it "store forwarding_url only at first" do
    redirect_to edit_user_path(user)
    expect(session[:forwarding_url]).not_to eq(edit_user_url(user))
    log_in_as(user)
    expect(session[:forwarding_url]).to be_nil
  end
end
```

また、デフォルトでは View をレンダリングしない。
そのため、レンダリングされた結果のタイトルが意図したものかテストしたい等の場合は `render_views` を使う。[参考URL](https://relishapp.com/rspec/rspec-rails/docs/controller-specs/render-views)

```spec/controllers/static_pages_controller_spec.rb
require 'rails_helper'

RSpec.describe StaticPagesController, type: :controller do
  render_views

  let(:base_title) { 'Ruby on Rails Tutorial Sample App' }

  it "get root" do
    get :home
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>#{base_title}<\/title>/i)
  end

  it "get home" do
    get :home
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>#{base_title}<\/title>/i)
  end

  it "get help" do
    get :help
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>Help | #{base_title}<\/title>/i)
  end

  it "get about" do
    get :about
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>About | #{base_title}<\/title>/i)
  end

  it "get contact" do
    get :contact
    expect(response.status).to eq(200)
    expect(response.body).to match(/<title>Contact | #{base_title}<\/title>/i)
  end
end
```

### request テストについて

Rails5 からは[Request specsを使うことが推奨](http://rspec.info/ja/blog/2016/07/rspec-3-5-has-been-released/)されています。

基本的なテスト内容は controller テストと同様ですが、controller テストでは controller の action を呼び出すのに対して、request テストではパスを指定する点が異なります。

get, post, patch, delete メソッドを実行するとテスト対象となるルーティングが行われ、対応するコントローラのアクションが実行されます。
これに応じて response.status が 200 であること、response.body に特定の文字列が含まれることなどがテスト出来ます。

尚、テスト対象と異なるコントローラのアクションを呼び出したい場合は `redirect_to` を使えばよい。

例: セッションを保存するSessionsControllerがあり、ログイン前にアクセスしたURLを記録する動作をテストする場合

```spec/requests/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :request do
  include RequestLoginHelper

  describe "GET /login" do
    it "render new" do
      get '/login'
      expect(response).to have_http_status(200)
    end
  end

  describe 'forwarding url' do
    let(:user) { FactoryBot.create(:user, name: 'michael') }

    it "should store forwarding_url only at first" do
      get '/login'
      redirect_to edit_user_path(user)
      expect(session[:forwarding_url]).not_to eq(edit_user_url(user))

      log_in_as(user)
      expect(session[:forwarding_url]).to be_nil
    end
  end
end
```

controller テストと違って render_views を書かなくても `response.body` により view に含まれる文字列をテストすることが出来ます。

```spec/requests/static_pages_spec.rb
require 'rails_helper'

RSpec.describe "StaticPages", type: :request do
  let(:base_title) { 'Ruby on Rails Tutorial Sample App' }

  describe "GET /" do
    it "should get root" do
      get '/'
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>#{base_title}<\/title>/i)
    end
  end

  describe "GET /home" do
    it "should get home" do
      get '/home'
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>#{base_title}<\/title>/i)
    end
  end

  describe "GET /help" do
    it "should get help" do
      get '/help'
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>Help | #{base_title}<\/title>/i)
    end
  end

  describe "GET /about" do
    it "should get about" do
      get "/about"
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>About | #{base_title}<\/title>/i)
    end
  end

  describe "GET /contact" do
    it "should get contact" do
      get "/contact"
      expect(response).to have_http_status(200)
      expect(response.body).to match(/<title>Contact | #{base_title}<\/title>/i)
    end
  end
end
```

### feature テストについて

capybara を使ったテストを記述する場合の、ブラウザ操作項目を記述する。

尚、capybara の公式ドキュメントは [Module: Capybara](http://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions) を参照。

- visit
    - 指定した URL パスへ HTTP GET によるアクセスを行う。
    - HTTP 応答された HTML 内容は次に記述する page に保持される。
- page
    - ブラウザが保持する DOM ツリーを保持するオブジェクト
    - ページ内に特定のメッセージが出力されることを確認するためには expect と have_text を組み合わせる
        - 例：`expect(page).to have_text("User was successfully created.")`

#### フォーム操作

- fill_in
    - input ボックスにテキストを入力する
- click_button
    - button をクリックする（submmitボタン等）
- attach_file
    - ファイルをアップロードする

#### ファイルをアップロードする

ファイルをアップロードする時には `attach_file` が使える。(参考情報: https://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions)

尚、テストでアップロードするためにファイルを用意する場合は fixture ファイルとして用意する。
デフォルトで fixture ファイルの保存先は `spec/fixtures/files/` 配下である。
※ 保存先を変更したい場合は RSpec.config.file_fixture_path の値を変更する。(参考URL: https://relishapp.com/rspec/rspec-rails/v/3-8/docs/file-fixture)

`spec/fixtures/files/rails.png` を保存した場合、 `file_fixture("rails.png")` でアクセスできる。
テキストファイルは `file_fixture("some.txt").read` で読み込める。

```ruby:spec/features/microposts_interface.rb
  it "micropost interface" do
    act_as(user) do
      visit root_path
      expect(page).to have_xpath("//div[@class='pagination']")

      # 有効な送信
      content = 'This micropost really ties the room toghether'
      picture = file_fixture("rails.png")
      expect(-> {
        within '#micropost' do
          fill_in 'micropost_content', with: content
          attach_file 'micropost_picture', picture
        end
        click_button 'Post'
      }).to change(Micropost, :count).by(1)
    end
  end
```

#### マッチャ

- have_content
    - page に含まれるコンテンツを比較する
- have_xpath
    - page に含まれるコンテンツを xpath で選択する
    - デフォルトでは非表示 DOM は検索されない
        - 非表示 DOM を検索する場合は visible: false をつける

#### リダイレクトをテストする

visit を使って遷移した場合、リダイレクト先へ自動で遷移します。
そのため、リダイレクトをテストする場合は change マッチャを使って `current_path` の変化を調べるとよいでしょう。(参考情報: https://ja.stackoverflow.com/questions/21673/capybara%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6-redirect%E5%85%88%E3%81%AE%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E6%A4%9C%E8%A8%BC%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)

```ruby:spec/features/microposts_interface.rb
RSpec.feature "MicropostsInterface", type: :feature do

  it "micropost interface" do
      # 有効な送信
      content = 'This micropost really ties the room toghether'
      picture = file_fixture("rails.png")
      expect(-> {
        within '#micropost' do
          fill_in 'micropost_content', with: content
          attach_file 'micropost_picture', picture
        end
        click_button 'Post'
      }).to change(Micropost, :count).by(1) && change { current_path }.to(root_path)
      expect(user.microposts.first.picture?).to be_truthy
      expect(page).to have_content(content)
  end
end
```

#### ヘルパーを読み込む

特定のユーザによる操作をテストする場合、ログイン・ログアウトを毎回記述するのは DRY ではありません。

ヘルパーにログイン・ログアウト・特定のユーザによるログイン操作を行うためのメソッドを用意して読み込ませるのがよいでしょう。(参考情報: https://qiita.com/jnchito/items/a8360e5e7a829d1e19b2)

## サンプルテスト

前提として、scaffold で User モデル（name, email属性を持つ）を作成しています。
scaffold を使ったモデルの作り方は別記事 [Ruby on Rails での best_in_place 事始め](https://qiita.com/tatsurou313/items/3391405f101f203d8bb5) を参照のこと。

```rb:spec/features/user_spec.rb
require "rails_helper"

RSpec.feature "user management", :type => :feature do
  scenario "End User creates a new user" do
    visit "/users/new"

    fill_in "user_name", :with => "test_name"
    fill_in "user_email", :with => "test@example.com"
    click_button "Create User"

    expect(page).to have_text("User was successfully created.")
  end
end
```

## トラブルシュート

RSpec を使う上でエラーが発生した場合等のトラブルシュートを見つけ次第記述していく。（見つけ次第、適宜追記予定）

## rspecコマンド実行時のfeatureテストにおけるエラー

### capybara 不足によるエラー

```sh:capybara不足によるエラー
Pending: (Failures listed here are expected and do not affect your suite's status)
 
  1) user management End User creates a new user
     # Feature specs require the Capybara (http://github.com/jnicklas/capybara) gem, version 2.2.0 or later. We recommend version 2.4.0 or later to avoid some deprecation warnings and have support for `config.expose_dsl_globally = false`.
     # ./spec/features/user_spec.rb:4
 
 
Finished in 0.01628 seconds (files took 4.12 seconds to load)
1 example, 0 failures, 1 pending
```

- 原因
    - capybaraがインストールされていない
- 対策
    - Gemfile の test グループに capybara を記述して bundle install する
- 方法
    1. Gemfile に capybara を記述（バージョンは適宜変更）
    2. `bundle install` コマンドを実行

```rb:Gemfile
group :test do
    ・・・
  gem 'capybara', '~> 2.8'
end
```

## click_buttonのメソッド不足によるエラー

```rb:click_buttonのメソッド不足によるエラー
  1) user management End User creates a new user
     Failure/Error: click_button "Create User"
     
     NoMethodError:
       undefined method `normalize_params' for Rack::Utils:Module
     # ./spec/features/user_spec.rb:9:in `block (2 levels) in <top (required)>'
```

- 原因
    - https://github.com/teamcapybara/capybara/issues/1592
- 対策
    - capybara バージョンを 2.8 以上にする