Ruby
Rails
RSpec

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

More than 3 years have passed since last update.


はじめに

3月頃,『【Rails】RSpecでWeb APIのテストでハマったところ①』という初心者丸出しな記事を書きました.

あれから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).


- Rails API Testing Best Practices


Request Specって? だいたいこんな感じ.


request-specという、HTTPにおけるリクエストとレスポンスの組み合わせを、言わばブラックボックスとして扱うテスト形式の呼び名があります。リクエストを入力、レスポンスを出力として扱い、ある入力に対して期待される出力が返されるかどうかをテストします。

- RSpecでRequest Describer - Qiita



前準備

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


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を必要としないテストを書きやすくなるんだとか。

- RSpec 3 時代の設定ファイル rails_helper.rb について - willnet.in



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です(違ってたらごめんなさい).



  • 1テストケースで1リクエスト

  • 内部でRouter/Dispatcher、Controller、Modelがどうなっているかというのはテストしていない

  • テストケースのネストの深さを均一にする ...(以下省略)

- 体育の日って高速に唱えるとテストの日に聴こえる - ✘╹◡╹✘


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


describeメソッドを利用して記述した文章を元にHTTPリクエストを送ってくれる

- RSpecでRequest Describer - Qiita


簡単に言うとdescribe 'GET /users/:id' do ... endとしたときにsubject内でリクエストを発行してくれる,というものになってます.

(以下のコードは元記事のshouldexpectに変えただけ)

# 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氏から直接回答をいただくことが出来ました.

非常に丁寧に解説いただきありがとうございました.


Parameterは?

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

let(:params) do

{ user: { name: 'izumin5210' } }
end

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

let(:params) do

{ user: FactoryGirl.attributes_for(:user) }
end


Headerは?

headersに格納.

let(:headers) do

{ 'Content-Type' => 'application/json',
'Accept' => 'application/json'
}
end

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

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を作って呼び出したら未認証時のテストも出来る(もうちょいまともな書き方できるかも).

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を使ったり,リクエスト時の共通処理をマクロに切り出すことで記述量を減らしてスリムで怠惰なテストを目指しましょう.

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@jnchitoさんおすすめgemです.

結構クセあるので最初は戸惑いましたが,今となっては手放せない感じ.

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に格納する場合,それを検査するためのマッチャ用意しとくと捗りません?

エラーメッセージを入れとくパスなんてアプリ内で統一していることが多そうなので…

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

使い方はこんな感じ.

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(%("そんなユーザおらへんでw")).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

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


この章のまとめ


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

autodocはテストケースに:autodocってつけるだけでテスト実行時にドキュメントを生成してくれる神gemです.

最近RSpec 3対応したようです.ありがとうございます!!!

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

異常系のドキュメントまで生成してるとごちゃごちゃしちゃいそうなので,正常系だけでいいかな…

タグ代わりにもなりますし.



  • 正常系にタグを付ける

- 体育の日って高速に唱えるとテストの日に聴こえる - ✘╹◡╹✘



この章のまとめ

autodocは神.


謝辞

RequestDescriber及びautodocの作者であり僕の疑問にも丁寧に答えてくださった @r7kamura さん,『Everyday Rails - RSpecによるRailsテスト入門』の訳者の1人であり超初心者だった僕にテストの基本を教えてくださった @jnchito さん,ありがとうございました.


参考文献


テスト全般


request specについて


autodoc関連


gemなど