【Rails】RSpecと三種の神器でらくちんWeb APIテスト

# はじめに

3月頃，『[【Rails】RSpecでWeb APIのテストでハマったところ①](http://qiita.com/izumin5210/items/e27ab2987683084fb009)』という~~初心者丸出しな~~記事を書きました．
あれから9ヶ月ほど，お仕事としてRailsに触れたりしたため知見・スキルも向上してきたと思います．

そして今，前述の記事を見直したところ恥ずかしくて顔を覆いたくなる感じになったので改めてWeb APIのテストについて書いていきます．

# APIのテスト？

そもそもWeb APIのテストはどこに書くの？ってところからですが…

Controller SpecではなくRequest Specに書いていきます．

> Use RSpec Request Specs
> 
> Since we’ve established that we’ll be using Rack::Test to drive the tests, RSpec request specs make the most sense. There’s no need to get fancy and add extra weight to your testing tools for this.
>
> > Request specs provide a thin wrapper around Rails’ integration tests, and are designed to drive behavior through the full stack, including routing (provided by Rails) and without stubbing (that’s up to you).
> 
> \- <cite>[Rails API Testing Best Practices](http://matthewlehner.net/rails-api-testing-guidelines/)</cite>

Request Specって？ だいたいこんな感じ．

> request-specという、HTTPにおけるリクエストとレスポンスの組み合わせを、言わばブラックボックスとして扱うテスト形式の呼び名があります。リクエストを入力、レスポンスを出力として扱い、ある入力に対して期待される出力が返されるかどうかをテストします。
> \- <cite>[RSpecでRequest Describer - Qiita](http://qiita.com/r7kamura/items/a1ab81561cb6accae558)</cite>

# 前準備

必要なGemとRSpecまわりの設定たち．

```ruby:Gemfile
group :test do
  gem 'rspec-rails'
  gem 'factory_girl_rails'
  gem 'database_rewinder'
  gem 'rspec-request_describer'
  gem 'autodoc'
  gem 'json_spec'
end
```

Railsまわりの設定だから`rails_helper.rb`…でいいのかな？

> `spec/rails_helper.rb` に Rails 特有の設定を書き、`spec/spec_helper.rb`には RSpec の全体的な設定を書く、というようにお作法が変わるそうです。これによって、Railsを必要としないテストを書きやすくなるんだとか。 
> \- <cite>[RSpec 3 時代の設定ファイル rails_helper.rb について - willnet.in](http://www.willnet.in/126)</cite>

```ruby:rails_helper.rb
RSpec.configure do |config|

  config.include JsonSpec::Helpers
  config.include RSpec::RequestDescriber, type: :request
  config.include RequestHelpers, type: :request
  config.include RequestMacros, type: :request

  config.before :all do
    FactoryGirl.reload
  end

  config.before :suite do
    DatabaseRewinder.clean_all
  end

  config.after :each do
    DatabaseRewinder.clean
  end

  Autodoc.configuration.toc = true
end
```

# Request Spec三種の神器

Request SpecでWeb APIのテストをする際に，絶対に必要な超便利gemです（自分が勝手に言ってるだけですが…）．

1つずつ紹介．

## RequestDescriber でテストをキレイにシンプルに

@r7kamura氏が挙げるRequest Specの慣習をもとに，よりテストコードを簡略化するためのgemが[RequestDescriber](https://github.com/r7kamura/rspec-request_describer)です（違ってたらごめんなさい）．

> * 1テストケースで1リクエスト
> * 内部でRouter/Dispatcher、Controller、Modelがどうなっているかというのはテストしていない
> * テストケースのネストの深さを均一にする ...（以下省略）
> 
> \- <cite>[体育の日って高速に唱えるとテストの日に聴こえる - ✘╹◡╹✘](http://r7kamura.hatenablog.com/entry/2013/10/14/134855)</cite>

このライブラリでもっとも特徴的かつ重要な機能は以下になります．

> describeメソッドを利用して記述した文章を元にHTTPリクエストを送ってくれる
> \- <cite>[RSpecでRequest Describer - Qiita](http://qiita.com/r7kamura/items/a1ab81561cb6accae558#%E4%BE%8B%E3%81%88%E3%81%B0request-describer%E3%82%92%E4%BD%BF%E3%81%86)</cite>

簡単に言うと`describe 'GET /users/:id' do ... end`としたときに`subject`内でリクエストを発行してくれる，というものになってます．
（以下のコードは元記事の`should`を`expect`に変えただけ）

```rb
# describeの内容を解析して subject { get "/users/#{id}" } がセットされる
describe 'GET /users/:id' do
  let(:id) { FactoryGirl.create(:user).id }

  context 'with invalid id' do
    let(:id) { 'invalid_id' }
    # is_expectedでsubjectを呼び出す
    it { is_expected.to eq 404 }
  end

  context 'with valid id' do
    # subjectを評価後，reponse.bodyの内容を検証
    it 'returns a user' do
      is_expected.to eq 200
      res = response.body
      expect(res).to ...
    end
  end
end
```

ただ，「これだと`subject`を呼ばないとresponse bodyが検証できない，1つの`it`に1つの`expect`では？」という疑問を持つ方もいるかもしれません．実際僕はそう思ってツイートしたところ， @r7kamura氏から直接回答をいただくことが出来ました．
非常に丁寧に解説いただきありがとうございました．

<blockquote class="twitter-tweet" lang="en"><p><a href="https://twitter.com/izumin5210">@izumin5210</a> 全てのテストケースで必ずstatusを検査するようにしようという意図があってそうしていました。subjectを評価するタイミングは少しシビアで、他にbeforeが定義されているとそちらを先に呼んでほしいため、ライブラリ側で評価することができませんでした</p>&mdash; コンテナ (@r7kamura) <a href="https://twitter.com/r7kamura/status/522575225955708928">October 16, 2014</a></blockquote>

<blockquote class="twitter-tweet" lang="en"><p><a href="https://twitter.com/izumin5210">@izumin5210</a> 確かに1つのテストケースで1つの検査をするのが理想的ですが、記述量と処理速度のコスパを考えてそこに落ち着いている感じです?</p>&mdash; コンテナ (@r7kamura) <a href="https://twitter.com/r7kamura/status/522583469310758912">October 16, 2014</a></blockquote>

<blockquote class="twitter-tweet" lang="en"><p><a href="https://twitter.com/izumin5210">@izumin5210</a> request specが非常に重い処理であるということと、APIは正常系のレスポンスボディの形があまり変化しないこと、異常系のテストが増えがちでステータスコードが境界値となりやすいことからその辺りが落とし所になっています</p>&mdash; コンテナ (@r7kamura) <a href="https://twitter.com/r7kamura/status/522583855245451264">October 16, 2014</a></blockquote>

### Parameterは？

`params`という変数に格納しておいたらそれを使ってくれる．

```rb
let(:params) do
  { user: { name: 'izumin5210' } }
end
```

FactoryGirlつかうならこんな感じ？

```rb
let(:params) do
  { user: FactoryGirl.attributes_for(:user) }
end
```

### Headerは？

`headers`に格納．

```rb
let(:headers) do
  { 'Content-Type' => 'application/json',
    'Accept' => 'application/json'
  }
end
```

以下の様なmacroを定義しておけば呼び出すだけでOK．

```rb
module RequestMacros
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def application_json
      before do
        set_content_type 'application/json'
        set_accept 'application/json'
      end
    end
  end
end
```

### 認証は？

下のようなmacroを作って呼び出したら未認証時のテストも出来る（もうちょいまともな書き方できるかも）．

```rb
module RequestMacros
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def context_user_authenticated(*user_traits, &block)
      context('with authentication') {
        # 認証に関する処理をbeforeとかに突っ込んどく
      }.class_eval &block

      context 'without authentication' do
        it { is_expected.to eq 401 }
      end
    end
  end
end
```


### この章のまとめ

[**RequestDescriber**](https://github.com/r7kamura/rspec-request_describer)を使ったり，リクエスト時の共通処理をマクロに切り出すことで記述量を減らしてスリムで怠惰なテストを目指しましょう．

```rb
require 'rails_helper'

describe 'User resource', type: :request do
  # Content-Type，Acceptの設定
  application_json

  describe 'GET /users' do
    # 認証が必要なAPIのテスト
    context_with_authenticated do
      # テストケースはブロック内に書ける
      it { is_expected.to eq 200 }
    end
  end
end
```

## json_specでresponse検査

[json_spec](https://github.com/collectiveidea/json_spec)，@jnchitoさんおすすめgemです．
結構クセあるので最初は戸惑いましたが，今となっては手放せない感じ．

<blockquote class="twitter-tweet" data-conversation="none" data-cards="hidden" lang="en"><p><a href="https://twitter.com/izumin5210">@izumin5210</a> ちなみに僕はjson_specっていうgemを使っています。最初は慣れるまでちょっと時間がかかったけど、コツをつかめばなかなか便利なツールです。 <a href="https://t.co/cVrDHrmhgl">https://t.co/cVrDHrmhgl</a></p>&mdash; Junichi Ito (伊藤淳一) (@jnchito) <a href="https://twitter.com/jnchito/status/448193107306106881">March 24, 2014</a></blockquote>

```rb
describe 'GET /users/:id' do
  let(:user) { FactoryGirl.create(:user) }
  let(:id) { user.id }

  context 'with valid id' do
    it 'returns a user' do
      is_expected.to eq 200
      body = response.body
      # {
      #   "user": {
      #     "name": "izumin5210",
      #     "email": "izumin5210@example.com"
      #   }
      # }

      # user というパスが存在するか？
      expect(body).to have_json_path('user')
      # user/id の内容は user.id と一致するか
      expect(body).to be_json_eql(user.id).at_path('user/id')
      # user/name の内容は "user.name" と一致するか（ダブルクオートも含め）
      expect(body).to be_json_eql(%("#{user.name}")).at_path('user/name')
      # ...
    end
  end
end
```

### エラーの検査

エラーメッセージをrequest bodyに格納する場合，それを検査するためのマッチャ用意しとくと捗りません？
エラーメッセージを入れとくパスなんてアプリ内で統一していることが多そうなので…

```rb
ERROR_PATH = 'errors'

# エラーメッセージが存在するか（ errors がbodyに含まれるか）
RSpec::Matchers.define :have_errors do |expected|
  match do |actual|
    expect(actual).to have_json_path(ERROR_PATH)
  end

  failure_message do
    %(expected to have error\n) +
    %(got no errors)
  end 

  failure_message_when_negated do
    %(expected to not have errors\n) +
    %(got errors)
  end
end

# 指定したパスにメッセージが入ってるか
RSpec::Matchers.define :have_error_messages do |expected|
  match do |actual|
    expect(actual).to have_errors
    expect(actual).to have_json_path(@path)
    messages = (expected.is_a? Array) ? expected : [expected]
    messages = messages.map { |msg| %("#{msg}") }.join(',')
    expect(actual).to be_json_eql(%([#{messages}])).at_path(@path)
  end

  failure_message do
    %(expected "#{expected}"\n) +
    %(got "#{JSON.parse(actual)[ERROR_PATH][MESSAGE_PATH]}")
  end 

  failure_message_when_negated do
    %(expected not "#{expected}"\n) +
    %(got "#{JSON.parse(actual)[ERROR_PATH][MESSAGE_PATH]}")
  end

  chain :at_path do |path|
    @path = "#{ERROR_PATH}/#{path}"
  end
end
```

使い方はこんな感じ．

```rb
describe 'GET /users/:id' do
  let(:id) { FactoryGirl.create(:user).id }

  context 'with invalid id' do
    let(:id) { 'invalid id' }
    it do
      is_expected.to eq 404
      body = response.body
      # エラーメッセージの検査
      expect(body).to have_error_messages(%("そんなユーザおらへんでｗ")).at_path('user')
    end
  end

  context 'with valid id' do
    it 'returns a user' do
      is_expected.to eq 200
      res = response.body
      # エラーメッセージがないことをチェック
      expect(res).to_not have_errors
      # expect(res).to ...
    end
  end
end
```

これが人生初マッチャでした．

### この章のまとめ

<blockquote class="twitter-tweet" lang="en"><p><a href="https://twitter.com/izumin5210">@izumin5210</a> するべきかどうかはケースバイケースです・・・っていうと回答にならないので、yes/noで答えるならyesかな～。というのも画面に比べて、APIは問題の発生に気付きにくい＆手作業+目視での確認が面倒なので、RSpecで品質を担保させた方が効率的だから。</p>&mdash; Junichi Ito (伊藤淳一) (@jnchito) <a href="https://twitter.com/jnchito/status/448192723736997888">March 24, 2014</a></blockquote>

## autodoc でドキュメント自動生成

[**autodoc**](https://github.com/r7kamura/autodoc)はテストケースに`:autodoc`ってつけるだけでテスト実行時にドキュメントを生成してくれる神gemです．
最近RSpec 3対応したようです．ありがとうございます！！！

```rb
describe 'GET /users/:id' do
  let(:id) { FactoryGirl.create(:user).id }

  context 'with valid id' do
    it 'returns a user', :autodoc do
      is_expected.to eq 200
      res = response.body
      expect(res).to_not have_errors
      # expect(res).to ...
    end
  end
end
```

異常系のドキュメントまで生成してるとごちゃごちゃしちゃいそうなので，正常系だけでいいかな…
タグ代わりにもなりますし．

> * 正常系にタグを付ける
> 
> \- <cite>[体育の日って高速に唱えるとテストの日に聴こえる - ✘╹◡╹✘](http://r7kamura.hatenablog.com/entry/2013/10/14/134855)</cite>

### この章のまとめ

[**autodoc**](https://github.com/r7kamura/autodoc)は神．

# 謝辞

[**RequestDescriber**](https://github.com/r7kamura/rspec-request_describer)及び[**autodoc**](https://github.com/r7kamura/autodoc)の作者であり僕の疑問にも丁寧に答えてくださった @r7kamura さん，『[**Everyday Rails - RSpecによるRailsテスト入門**](https://leanpub.com/everydayrailsrspec-jp)』の訳者の1人であり超初心者だった僕にテストの基本を教えてくださった @jnchito さん，ありがとうございました．

# 参考文献

## テスト全般

* [Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp)（書籍）
* [Rails でつくる API のテストの書き方（RSpec + FactoryGirl） - 彼女からは、おいちゃんと呼ばれています](http://blog.inouetakuya.info/entry/2013/10/27/200111)
* [RSpec 3 時代の設定ファイル rails_helper.rb について - willnet.in](http://www.willnet.in/126)

## request specについて
* [Rails API Testing Best Practices](http://matthewlehner.net/rails-api-testing-guidelines/)
* [体育の日って高速に唱えるとテストの日に聴こえる - ✘╹◡╹✘](http://r7kamura.hatenablog.com/entry/2013/10/14/134855)
* [RSpecでRequest Describer](http://qiita.com/r7kamura/items/a1ab81561cb6accae558)


## autodoc関連

* [Autodoc - r7km/s](http://r7kamura.github.io/2013/12/01/autodoc.html)
* [Rails でつくる API のドキュメントを自動生成してくれる autodoc がすごい - 彼女からは、おいちゃんと呼ばれています](http://blog.inouetakuya.info/entry/2013/10/20/132928)

## gemなど

* [r7kamura/rspec-request_describer](https://github.com/r7kamura/rspec-request_describer)
* [r7kamura/autodoc](https://github.com/r7kamura/autodoc)
* [collectiveidea/json_spec](https://github.com/collectiveidea/json_spec)