Rails でつくる API のテストの書き方(RSpec + FactoryGirl)
最近 Rails で API をつくりはじめました。1か月テスト書きまくって、だいたい書き方が落ち着いてきたので共有します。
もっとこうした方が良くない?というのをコメントまたははてブコメントしてくださったら泣いて喜びます!
環境は下記のとおりです。
* rails 4.0.0 * rspec-rails 2.14.0 * factory_girl_rails 4.2.0
もくじ
Rails でつくる API のテストの書き方(RSpec + FactoryGirl) 1. ベストプラクティス 2. RSpec の Request spec で書く 3. Capybara は使わない 4. GET のテスト例 1(データを 1件取得) (1) ステータスコードの明示 (2) モデルの個別の内容を返すときは、属性を列挙して確認 5. GET のテスト例 2(データを一覧取得) (1) 属性を列挙して確認しなくてよい (2) FactoryGirl の定義内容を逐一確認しなくて済むようにする 6. POST のテスト例 1(正常系) (1) FactoryGirl.attributes_for (2) post を before ブロックに含めない (3) ステータスコードと success/failed を response.body に埋める 7. POST のテスト例 2(異常系) (1) エラーメッセージの中身を確認する (2) context や it を日本語で書く 8. PUT のテスト例 9. API ドキュメントを自動生成する autodoc 10. 返ってくる json のチェックをサポートする json_expressions
1. ベストプラクティス
ひととおり Google 先生にたずねてみて、これがたぶんベストプラクティスだよね、と思ったのが下記の記事。タイトルもそのままなのですが。
今回はこの記事の中からいくつかポイントをピックアップして、あと、少し tips を加えたかたちにしたいと思います。
2. RSpec の Request spec で書く
まず基本的に、特別な理由がなければ RSpec の Request spec で書くのが良いと思います。日本語の記事で Controller spec を使っているのが散見されたのですが、それだと対応できない場合があります。
例えば、scaffold で生成されるような、典型的な create メソッド。@video.save
に成功したら render action: 'show'
するようにしていますが、これを Controller spec で response.body を確認すると空文字列になっています。したがって、正しく @video
の内容が返ってくることをテストできません。
# app/controllers/api/videos_controller.rb # POST /videos.json def create @video = Video.new(video_params) respond_to do |format| if @video.save format.json { render action: 'show', status: :created, location: @video } else format.json { render json: @video.errors, status: :unprocessable_entity } end end end
このようなケースに対応するためにも、Request specs を使うのが良いと思います。
補足すると、Controller spec は単体テストで Request spec は結合テストで、そもそも役割が違うのですが、ふたつともに書くと、だいたい冗長になるので、省エネのためにも、僕は Request spec しか使っていないです。
3. Capybara は使わない
Capybara の作者 Jonas Nicklas さんが下記のように書いています。
Do not test APIs with Capybara. It wasn’t designed for it.
代わりに Rack::Test を使え、と。
Capybara は、バージョン 2.0.0 から spec/requests
を見なくなったので(代わりに spec/features
を見る)、下記のようにして、ウェブページのテストと棲み分けしています。
spec/spec_helper.rb
にinclude Capybara::DSL
と書かない- API のテストは
spec/requests
に置く - ウェブページのテストは
spec/features
に置く = Capybara 使う
4. GET のテスト例 1(データを 1件取得)
先に例を示します。
describe 'GET /api/videos/:id.json' do before do @video = FactoryGirl.create(:video) get "/api/videos/#{@video.id}.json" end it '200 OK が返ってくる' do expect(response).to be_success expect(response.status).to eq(200) end it '動画情報を取得できる' do json = JSON.parse(response.body) expect(json['title']).to eq(@video.title) expect(json['status']).to eq(@video.status) # expect(json['time']).to ... # expect(json['tags']).to ... end end
典型的なケースですね。少し解説を加えます。
(1) ステータスコードの明示
まず、expect(response).to be_success
だけではなく、expect(response.status).to eq(200)
とステータスコードを明示しているのなぜか?
それは、json を受け取る側で、ステータスコードを参照して処理を分岐させているためです。
つまり 200 でも 201 でも be_success になるんだけど、このケースでは 200 を返すよということを json を受け取る側をつくる開発者が、テストコードを見れば分かるようにしています(実際はあんまり見てなくて、他の方法でその開発者に伝えているのですが。後述します)
(2) モデルの個別の内容を返すときは、属性を列挙して確認
(一覧ではなくて)個別の情報を返すときは、面倒だけれども、属性を列挙して確認するようにしています。
たいていの場合、モデルの属性を全部返すわけではないでしょうから、返す属性と返さない属性がきちんと仕様どおりになっているか確認したり、返す属性の中身が正しいか確認したりします。
5. GET のテスト例 2(データを一覧取得)
次は一覧取得の API のテスト例です。
describe 'GET /api/videos.json' do before do @video1 = FactoryGirl.create(:video, title: 'すぐにまたがる家庭教師') @video2 = FactoryGirl.create(:video, title: '夫のために身体を差し出した人妻') get '/api/videos.json' end it '200 OK が返ってくる' do expect(response).to be_success expect(response.status).to eq(200) end it '動画一覧情報を取得できる' do json = JSON.parse(response.body) expect(json).to match(/すぐにまたがる家庭教師/) expect(json).to match(/夫のために身体を差し出した人妻/) end end
(1) 属性を列挙して確認しなくてよい
Rails 4 標準で使われている jbuilder を例にすると、たいてい、下記のように、個別情報を partial して一覧で返すようにしていると思います。
json.partial! 'video', collection: @videos, as: :video
なので、モデルが複数個返ってきているかは確認する必要がありますが、個別の中身については、前述のテスト例 1 のケースで済ませているので、不要かと考えています。
(2) FactoryGirl の定義内容を逐一確認しなくて済むようにする
もしかしたら、下記のようにすると、同じタイトルの動画が 2つできてしまうかもしれません。
@video1 = FactoryGirl.create(:video) @video2 = FactoryGirl.create(:video)
その場合に下記のように書いても、動画が複数個返ってきたかは確認できません。
expect(json).to match(/#{@video1.title}/) expect(json).to match(/#{@video2.title}/)
したがって、タイトルを明示して確認するようにしています。factory :video
がどのように定義されているかを確認すれば済むのですが、当該テストの記述だけ見て判断できる方がより良いと思います。
なお、並び順も含めて確認したい場合は下記のような感じです。
expect(json[0]['title']).to eq('すぐにまたがる家庭教師') expect(json[1]['title']).to eq('夫のために身体を差し出した人妻')
6. POST のテスト例 1(正常系)
POST の正常系のテストの例。パラメータを POST したら、データが 1件登録されるケースを想定しています。
# # 3. 動画の登録 # describe 'POST /api/videos.json' do let(:path) { '/api/videos.json' } context '3-1. パラメータが正しいとき' do before do user = FactoryGirl.create(:user) sign_in user @params = { video: FactoryGirl.attributes_for(:video) } end it '201 Created が返ってくる' do post path, @params expect(response).to be_success expect(response.status).to eq(201) end it 'ステータスコードと success/failed を response.body に埋める' do post path, @params json = JSON.parse(response.body) expect(json['status']).to eq(201) expect(json['result']).to eq('success') expect(json['error_messages']).to eq(nil) end it 'Video が 1 増える' do expect { post path, @params }.to change(Video, :count).by(1) end end context '3-2. 動画タイトルが入っていないとき' do # ...
(1) FactoryGirl.attributes_for
POST するときはパラメータをハッシュで指定するのですが、FactoryGirl を使っている場合は、定義している内容を下記のようにしてもってこれるので便利です。
@params = { video: FactoryGirl.attributes_for(:video) }
(2) post を before ブロックに含めない
下記のようにデータが増減するのを確認することがよくあるので、post は before ブロックに含めないようにしています。
expect { post path, @params }.to change(Video, :count).by(1)
(3) ステータスコードと success/failed を response.body に埋める
当初 POST が成功したら、登録したデータを返すようにしていたのですが、iPhone アプリ側の開発者から要望がありました。
ヤリ手の彼がそういうのだからきっとそうなんだと思います。一方で API 側の工数は別にそのせいで増えたりしないので(jbuilder 便利)、そのような実装にしています。参考まで。
# app/view/api/videos/create.json.jbuilder json.status response.status json.result @result json.error_messages @error_messages
7. POST のテスト例 2(異常系)
POST の異常系のテストの例。データを 1件登録するために POST したんだけど、パラメータに誤りがあるケースを想定。
# # 3. 動画の登録 # describe 'POST /api/videos.json' do let(:path) { '/api/videos.json' } context '3-1. パラメータが正しいとき' do # ... end context '3-2. 動画タイトルが入っていないとき' do before do user = FactoryGirl.create(:user) sign_in user @params = { video: FactoryGirl.attributes_for(:video, title: '') } end it '422 Unprocessable Entity が返ってくる' do put path, @params expect(response).not_to be_success expect(response.status).to eq(422) end it 'エラーメッセージが返ってくる' do put path, @params json = JSON.parse(response.body) expect(json['status']).to eq(422) expect(json['result']).to eq('failed') expect(json['error_messages']).to eq(['動画タイトルを入力してください']) end it 'Video が増減しない' do expect { put path, @params }.not_to change(Video, :count) end end # ... end
(1) エラーメッセージの中身を確認する
異常系のテストは、エラーメッセージがきちんと返っているか?を必ず確認するようにしています。ここはユーザに直接見えるところなので、重点を置いている感じ。
たまに i18n の記述漏れでエラーメッセージに英語が混ざっている場合もあるので、それも含めてチェックしている感じです。
(2) context や it を日本語で書く
ここはまあ、意見の分かれるところでしょうか。特にオープンソースで書くコードか、業務で書くコードか、で。
逐一やっているわけではないですが、ディレクターに
こういうケースではこういうエラーメッセージになりますが、それでよいですか?
と確認したいことがあります(というか、確認すべきです)。そのときに、別途「エラーメッセージ一覧」とか資料をつくるのだるいので、テストコードを一緒に見ながら説明するようにしています。
- context '3-2. 動画タイトルが入っていないとき' do
- it 'エラーメッセージが返ってくる' do
というふうに日本語で書いていたり、項番を振っているのは、そのとき説明しやすいようにするためです。
ただ、それが主な理由ですが、関係するエンジニアがいまのところ全員日本人なので、テストコードを見て仕様を把握するスピードを上げるため、というのもあります。
8. PUT のテスト例
PUT のテスト例。もうだいたい言ったので、サンプルコードだけ示します。既に登録されているデータ 1件を更新するケース。
# # 4. 動画の更新 # describe 'PUT /api/videos/:id.json' do context '4-1. パラメータが正しいとき' do before do user = FactoryGirl.create(:user) sign_in user @video = FactoryGirl.create(:video) @params = { video: FactoryGirl.attributes_for(:video, title: '新しいタイトル') } @path = "/api/videos/#{video.id}.json" end it '200 OK が返ってくる' do put @path, @params expect(response).to be_success expect(response.status).to eq(200) end it 'ステータスコードと success/failed を response.body に埋める' do put @path, @params json = JSON.parse(response.body) expect(json['status']).to eq(200) expect(json['result']).to eq('success') expect(json['error_messages']).to eq(nil) end it 'Video が更新される' do put @path, @params expect(@video.reload.title).to eq('新しいタイトル') end it 'Video が増減しない' do expect { put @path, @params }.not_to change(Video, :count) end end context '4-2. 動画タイトルが入っていないとき' do # ...
9. API ドキュメントを自動生成する autodoc
便利情報です。autodoc を使うと、
$ AUTODOC=1 bundle exec rspec/spec/requests/api/videos_spec.rb
とテストを実行するだけで、下記のような API ドキュメントを自動生成してくれます。
先日、紹介記事を書きましたので参考にしてください。
10. 返ってくる json のチェックをサポートする json_expressions
僕自身は使っていないのですが、json の中身が複雑なケースでは、json_expressions を使うと書きやすそうです。
# RSpec example describe UsersController, "#show" do it "returns a user" do # This is what we expect the returned JSON to look like pattern = { user: { id: :user_id, # "Capture" this value for later username: 'chancancode', # Match this exact string full_name: 'Godfrey Chan', email: 'godfrey@example.com', type: 'Administrator', points: Fixnum, # Any integer value homepage: /\Ahttps?\:\/\/.*\z/i, # Let's get serious created_at: wildcard_matcher, # Don't care as long as it exists updated_at: wildcard_matcher, posts: [ { id: Fixnum, subject: 'Hello world!', user_id: :user_id, # Match against the captured value tags: [ 'announcement', 'welcome', 'introduction' ] # Ordering of elements does not matter by default }.ignore_extra_keys!, # Skip the uninteresting stuff { id: Fixnum, subject: 'An awesome blog post', user_id: :user_id, tags: ['blog' , 'life'] }.ignore_extra_keys! ].ordered! # Ensure the posts are in this exact order } } server_response = get '/users/chancancode.json' server_response.body.should match_json_expression(pattern) end end
いま、ふわっとした仕様のもとで、コードを書き進めなたら仕様を固めていっているような感じなので、テーブル定義変更とか、URL 変更とかざらに発生しています。そんなときに
テスト書いてて良かったー
と、そのありがたみをヒシヒシと感じている今日この頃です。ではでは。