RSpec で oauth2 の API 呼び出しをモックする

# はじめに

OAuth 2.0 のトークンを使用して API にリクエストする機会が今までなかったのですが、先日、急に必要になったので困りました。ひとまずしのぎました。

そのついでに、RSpec で oauth2 のレスポンスをモックするにはどうすればよいのだろうと思ったので、試してみます。

X API v2 を題材にします。

oauth2 の gem を使用します。

```ruby:Gemfile
gem "oauth2", "~> 2.0"
```

# 実際にリクエストを送信する

まず、実際にリクエストを送信して本物の結果を取得するコードを書いてみます。

```ruby:spec/x_v2_oauth2_api_spec.rb
RSpec.shared_examples 'Actually sending the request' do
  describe '実際にリクエストを送信する処理が' do
    let(:oauth2_client) do
      options = { site: 'https://api.x.com' }
      OAuth2::Client.new(ENV.fetch('CLIENT_ID'), ENV.fetch('CLIENT_SECRET'), options)
    end

    let(:access_token) do
      OAuth2::AccessToken.from_hash(oauth2_client, { token: token_code })
    end

    it '成功すること' do
      response = access_token.get('/2/users/me')
      expect(response.parsed[:data][:id]).to eq '1111'
    end
  end
end

RSpec.describe 'XV2Oauth2Api' do
  let(:token_code) { 'aaa' }

  include_examples 'Actually sending the request'
end
```

`OAuth2::AccessToken` のインスタンスがリクエストを送信し、レスポンスを生成しています。このインスタンスをモックに差し替えることができれば、任意のレスポンスを返却させることができそうです。

# リクエストとレスポンスをモックする

`OAuth2::AccessToken` クラスの `from_hash` メソッドを上書きするコードを用意します。

```ruby:spec/oauth2_access_token_mock_context.rb
RSpec.shared_context 'with oauth2 access token mock' do
  let(:stub_access_token) do
    allow(OAuth2::AccessToken).to receive(:from_hash).and_return(mock_access_token)
  end

  let(:mock_access_token) do
    mock = instance_double('access_token')
    allow(mock).to receive(:get).and_return(OAuth2::Response.new(http_response))
    mock
  end

  let(:http_response) do
    mock = instance_double('http_response')
    allow(mock).to receive(:headers).and_return({ 'Content-Type' => 'application/json' })
    allow(mock).to receive(:status).and_return(200)
    allow(mock).to receive(:body).and_return(JSON.generate(response_data))
    mock
  end

  let(:response_data) do
    {
      data: {
        id: '1111',
        name: 'ユーザー1',
        username: 'user1'
      }
    }
  end
end
```

`OAuth2::AccessToken` の代わりのオブジェクトを生成し、自前で用意した `OAuth2::Response` インスタンスが返却されるようにしました。

使用してみます。

```ruby:spec/x_v2_oauth2_api_spec.rb
RSpec.shared_examples 'Mocking an Access Token' do
  describe 'アクセストークンのモックが' do
    include_context 'with oauth2 access token mock'

    before do
      stub_access_token
    end

    let(:oauth2_client) do
      options = { site: 'https://api.x.com' }
      OAuth2::Client.new(ENV.fetch('CLIENT_ID'), ENV.fetch('CLIENT_SECRET'), options)
    end

    let(:access_token) do
      OAuth2::AccessToken.from_hash(oauth2_client, { token: token_code })
    end

    it '成功すること' do
      response = access_token.get('/2/users/me')
      # puts response.inspect
      expect(response.parsed[:data][:id]).to eq('1111')
    end
  end
end

RSpec.describe 'XV2Oauth2Api' do
  let(:token_code) { 'aaa' }

  include_examples 'Mocking an Access Token'
end
```

「実際にリクエストを送信する」のコードと同じように見えますが、リクエストは送信されず、自前で用意した結果が返却されます。

`puts response.inspect` で返却値を確認すると、InstanceDouble オブジェクトが返却されていることがわかります。

# アプリケーションコード内で送信されるリクエストをモックする

アプリケーションコードが以下のように書かれているとします。

```ruby:app/util/oauth2_util.rb
class Oauth2Util
  def self.make_client
    options = { site: 'https://api.x.com' }
    OAuth2::Client.new(ENV.fetch('CLIENT_ID'), ENV.fetch('CLIENT_SECRET'), options)
  end

  def self.make_access_token(token_code)
    client = make_client
    options = { token: token_code }
    OAuth2::AccessToken.from_hash(client, options)
  end
end
```

```ruby:app/api/x_v2_oauth2_api.rb
class XV2Oauth2Api
  def initialize(access_token:)
    @access_token = access_token
  end

  def get_me
    @access_token.get('/2/users/me')
  end
end
```

独自クラス `XV2Oauth2Api` のインスタンスメソッドでリクエストが送信されています。その結果をモックできるかを確認します。

```ruby:spec/x_v2_oauth2_api_spec.rb
RSpec.shared_examples 'Mocking access tokens created in application code' do
  describe 'アプリケーションコード内で作られているアクセストークンのモックが' do
    include_context 'with oauth2 access token mock'

    before do
      stub_access_token
    end

    let(:x_v2_oauth2_api) do
      access_token = Oauth2Util.make_access_token(token_code)
      XV2Oauth2Api.new(access_token: access_token)
    end

    it '成功すること' do
      response = x_v2_oauth2_api.get_me
      # puts response.inspect
      expect(response.parsed[:data][:id]).to eq('1111')
    end
  end
end

RSpec.describe 'XV2Oauth2Api' do
  let(:token_code) { 'aaa' }

  include_examples 'Mocking access tokens created in application code'
end
```

「リクエストとレスポンスをモックする」の shared_context をそのまま使用しています。`before` で `OAuth2::AccessToken` クラスの `from_hash` メソッドが上書きされます。

こちらも、`puts response.inspect` でレスポンスがモックされていることが確認できます。

# おわりに

OAuth 2.0 のトークンを使用したリクエストと、そのレスポンスをモックすることができました。

通信処理があるとテストが難しくなりますが、モックする方法が確立できているとテストコードが書きやすくなります。

テストコード、がんばります。