LoginSignup
17
6

More than 5 years have passed since last update.

(Rails・RSpec)WebMockでAPIサーバーへリクエストを投げるコントローラのテストをする

Last updated at Posted at 2017-03-14

画面を提供するための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 メソッドに渡せば、期待した動作になります。

spec/support/fake_api.rb
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を必要なところでのみ使うようにしました。

spec/hoge_controller_spec.rb
# WebMockを使ってテストする
RSpec.describe HogeController do
  before do
    WebMock.enable!
    stub_request(:get, /example.com/).to_rack(FakeAPI)
  end

  after { WebMock.disable! }

  # テスト
end
spec/fuga_controller_spec.rb
# WebMockを使わないでテストする
RSpec.describe FugaController do
  before do
    WebMock.disable!
  end
  # テスト
end

これで、生HTTPリクエストを段階的にWebMockへ移行するための設定ができました :tada:

200以外のテストをする

APIからの404を受け取って404を表示したい場合のテストはどうしようか少し悩みましたが、sinatraのルーティングに少し条件を足して存在しないユーザーに対するリクエストを表現すれば404のテストになるのではないでしょうか。

spec/support/fake_api.rb
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
spec/controllers/users_controller_spec.rb
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 に渡すとかそこらへんは以下の請け売りです。

17
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
6