画面を提供するためのRailsアプリケーションで、テスト実行時に決まったJSONを返すAPI Mockみたいなものが欲しくなったので、調べました。ここで言う「画面を提供するためのRailsアプリケーション」とは何かというと、以下のようなアプリケーションです。
ブラウザ <-> 画面を提供するためのRailsアプリケーション <-> APIサーバー
ブラウザとAPIサーバーの間に挟まってるRailsアプリです。今回はこのAPIサーバーにあたる部分のモックが欲しくなりました。
画面を提供するだけのアプリにRailsを使うのは珍しいかもしれませんが、普通のRailsアプリケーションでも外部へHTTPリクエストを投げることはあるので、適宜ご自分のRailsアプリに必要な部分だけ抜き出して読んでいただけると役に立つかもしれません。
背景
これまでコントローラのテストでは、そのまま get :index
みたいなものを書いていたので、そのメソッドがAPIサーバーへリクエストを投げるものだった場合は、実際にHTTPリクエストを投げていました。
しかし、これだとAPIサーバーが不調だったりしたときにテストがコケる、ログインユーザーでテストしたいけど実データに依存したテストを書くしかない、みたいな状況でした。そもそもそれについて考えるのに時間がかかりそうだったのでテストを書くこと自体後回しにしていたら、あるタイミングで私の不注意が重なり、本番環境で画面がコケる(nilの存在しないメソッドを呼び出そうとするよくあるバグ)ことになってしまいました。
そこで今回、HTTPによるAPIとのやり取りの部分を実際のAPIサーバーに依存しない形でテストすることにしました。
WebMockでAPIサーバーへリクエストを投げるコントローラのテストをする
調べてみると、WebMockというgemがあることを知りました。
WebMockの使い方
ドキュメントにも書いてありますが、以下のようにして使います。
stub_request(:get, "www.example.com")
Net::HTTP.get("www.example.com", "/") # ===> Success
今回はAPIサーバーの代わりにつかいたいので、URIに応じたJSONファイルを返してほしいです。返すべきダミーのJSONファイルを用意して、こう書けます。
stub_request(:get, "www.example.com").to_return(json_file)
JSONを返すアプリケーションをSinatraで作って to_rack
に渡す
上記の to_return
でもいいんですが、JSONファイルを返すためのルーティングは分けて定義したいです。そこで、JSONファイルを返すための簡単なSinatraアプリケーションを作成して、それを to_rack
メソッドに渡せば、期待した動作になります。
require 'sinatra/base'
class FakeAPI < Sinatra::Base
get '/users' do
json_response 200, 'users.json'
end
get '/posts' do
json_response 200, 'posts.json'
end
private
def json_response(response_code, file_name)
content_type :json
status response_code
File.open(File.dirname(__FILE__) + '/fixtures/' + file_name, 'rb').read if file_name
end
end
stub_request(:get, "www.example.com").to_rack(FakeAPI)
JSONファイルの作り方
APIから取得した実際のレスポンスを spec/support/fixtures/users.json
のようにして置いています。
ダミーのレスポンスを返さないまま(FakeAPIでルーティングしないまま)リクエストを投げるとだいたいこういう形でSinatraの404エラーになるので、テスト実行して足りなかったら「ああ、そうだ」といってJSONファイルを足していくという感じになると思います。
WebMockを必要なところでのみ使う
ただ、すでに生HTTPリクエストを外部に送信するようなテストを書いていたので、構わずWebMockを自分がテストしたい部分で使おうとすると、他のテストがコケて以下のようなエラーが出ます。
WebMock::NetConnectNotAllowedError:
Real HTTP connections are disabled. Unregistered request: GET http://example.com/hoge with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Host'=>'example.com', 'User-Agent'=>'Hoge-Client'}
それらを全て書き換える(そこで必要なJSONファイルを用意したりする)のは大変だったので、WebMockを必要なところでのみ使うようにしました。
# WebMockを使ってテストする
RSpec.describe HogeController do
before do
WebMock.enable!
stub_request(:get, /example.com/).to_rack(FakeAPI)
end
after { WebMock.disable! }
# テスト
end
# WebMockを使わないでテストする
RSpec.describe FugaController do
before do
WebMock.disable!
end
# テスト
end
これで、生HTTPリクエストを段階的にWebMockへ移行するための設定ができました
200以外のテストをする
APIからの404を受け取って404を表示したい場合のテストはどうしようか少し悩みましたが、sinatraのルーティングに少し条件を足して存在しないユーザーに対するリクエストを表現すれば404のテストになるのではないでしょうか。
class FakeAPI < Sinatra::Base
UNDEFINED_USER_ID = 0
get '/users/:id' do |id|
if id == UNDEFINED_USER_ID
json_response 404
else
json_response 200, 'user.json'
end
end
end
context '存在しないユーザーへのリクエストの場合' do
it '404を返すこと' do
get :show, params { id: FakeAPI::UNDEFINED_USER_ID }
expect(response).to have_http_status(404)
end
end
課題
- ダミーのJSONファイルを用意するのが面倒
- ダミーのJSONファイルの命名方法に悩む
- リソースの属性値を1つだけテスト実行側で設定したいときにどう書けばいいのか
- 例:「名前が"Joe"であるユーザー」
- FactoryGirlで言うところの
user = build(:user, first_name: "Joe")
みたいな
参考
主にこちらを参考にさせて頂きました。Sinatraアプリを to_rack
に渡すとかそこらへんは以下の請け売りです。