【Rails】RSpecについて

RspecとはRubyプログラマー向けのBDDツールです。
ここでのBDDはテスト駆動開発、ドメイン駆動型設計、受入テスト型設計へのアプローチのこと。

#Rspec導入

```Gemgfile
group :development, :test do
  gem 'rspec-rails', '~> 3.6'
end
```
RspecのGemパッケージをインストールします。
developmentを追加している理由としてはRspecにはテストファイルを作成するgenaratorがあり、それ利用する時に便利だからです。

そしたら`$ bundle install`します。

##Springを使用したRSpecの導入
Springを使用してRSpecを実行することで一回の実行時間を短縮することができる。

```Gemfile
：
：
group :development, :test do
  gem 'spring-commands-rspec'
end
：
：
```
`$ bundle exec spring binstub rspec`を実行することで`bin/rspec`ファイルが作成されるので`bin/rspec`を使用してRSpecを実行するとSpringを使用することができます。

#Rspecの実行
インストールが完了した段階で`$ bundle exec rspec`でRSpecが実行できます。

#テスト作成
テストファイルはRubyで記載します。
RSpecはデフォルトで`spec` ディレクトリ配下のファイルをテストファイルと認識します。

RSpecの`generator`を使用してファイルを作成して編集します。

```
$ 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を使用して記述します。
テスト内容はitで記載して期待する動作はexpectを１つ含める。

```rb
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`に続いて動詞から始まる説明文で記述していた理由としては、記載した内容が表示されるため何のためにテストを実施したのかが実行結果を見ればわかるためです。

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

Finished in 0.02296 seconds (files took 0.35495 seconds to load)
1 example, 0 failures
```
またテストが特定の条件を想定する場合は`context`を使用してその条件を記載します。
ここで`describe`,`context`は入れ替えても実行できます。

```rb
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
```
##テストする対象
対象はモデル、ビュー、コントローラがあげられる。

・Model specs
→モデルのvalidation等をテストする。

・controller specs
→テストは RSpec.describe ${ControllerClass} do ～ end を使って記述する
※オプションとして type: :feature を指定する
Rails5 からはRequest specsを使うことが推奨されている)

・Request specs
→・統合テストの細かいラッパーを記述します。
　　→・基本的にスタブは使用しない。
　　 ・ルーティングとコントローラの双方の動作を記述する。
 ・テスト内容の例
　　→・１回のrequestを指定する。
　　 ・複数のrequestを複数のcontrollersにまたがって指定する。
　　 ・複数のrequestを複数の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 が統一されていると都合がよいため)

#テストファイルに記述する内容
テストする対象が決まったらテストの内容を記述していきます。
テストの内容は確認したいことを１つ記載する。
また、describeとitの内容を繋げて読むと文章になるようにit文は動詞から始める。

```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
```
##controllerテストについて
`get`, `post`, `patch`, `delete` メソッドを実行するとテスト対象となるコントローラに対してオプションで指定したアクションが実行される。
これに応じてresponse,statusが200であることをテストできる。

テスト対象と異なるコントローラのアクションを呼び出したいときは`redirect_to`を使用すれば良い。

```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` を使用する。

```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テストについて
基本的にはcontrollerと同様ですが、requestテストではcontrollerのactionを呼び出すのに対してrequestテストではパスを指定する点が異なります。

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

尚、テスト対象と異なるコントローラのアクションを呼び出したいときは`redirect_to`を使用します。

```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テストについて
capycaraを使用したテストを記述する場合の、ブラウザ操作項目を記述する。

・visit
 ・指定したURLパスへHTTP GETによるアクセスを行う
 ・HTTP応答されたHTML内容は次に記述するpageに保持される。

・page
 ・ブラウザが保持するDOMツリーを保持するオブジェクト
 ・ページ内に特定のメッセージが出力されることを確認するためには`expect` と`have_text`を組み合わせる。

##フォーム操作
・fill_in
 →・inputボックスにテキストを入力する

・click_button
 →・buttonをクリックする（submmitボタン等）

・attach_file
 →・ファイルをアップロードする

###ファイルをアップロードする
`attach_file`を使用するとファイルアップロードが行える。

尚、テストでアップーロードするためにファイルを用意する場合は`fixture`ファイルとして用意する。
デフォルトの保存先は`spec/fixtures/files/` 配下になる。
※保存先を変更したい場合は`RSpec.config.file_fixture_path `の値を変更する。

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

```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`の変化を調べると良いでしょう。

```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ではありません。
ヘルパーにログイン・ログアウト・特定のユーザーによるログイン操作を行うためのメソッドを用意して読み込ませるのが良いでしょう。