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

**画面を提供するための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があることを知りました。

- https://github.com/bblimke/webmock

## WebMockの使い方
ドキュメントにも書いてありますが、以下のようにして使います。

```rb
stub_request(:get, "www.example.com")
Net::HTTP.get("www.example.com", "/")    # ===> Success
```

今回はAPIサーバーの代わりにつかいたいので、URIに応じたJSONファイルを返してほしいです。返すべきダミーのJSONファイルを用意して、こう書けます。

```rb
stub_request(:get, "www.example.com").to_return(json_file)
```

## JSONを返すアプリケーションをSinatraで作って `to_rack` に渡す
上記の `to_return` でもいいんですが、JSONファイルを返すためのルーティングは分けて定義したいです。そこで、JSONファイルを返すための簡単なSinatraアプリケーションを作成して、それを `to_rack` メソッドに渡せば、期待した動作になります。

```rb: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
```

```rb
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のテストになるのではないでしょうか。

```rb: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
```

```rb: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` に渡すとかそこらへんは以下の請け売りです。

- [How to Stub External Services in Tests](https://robots.thoughtbot.com/how-to-stub-external-services-in-tests)