外部APIを利用したメソッドのテストコードを書く際、APIとの連携方法がテストのスコープ外になることがあります。
たとえば『API経由で受け取ったデータを正しく加工できているか』をテストする場合です。
この時のテストの関心はあくまで『データを加工するロジック』であるため『どのようにデータを取得してくるのか』という部分に関しては気にする必要がありません。
上記のようなテストでは外部APIとの連携部分をモックオブジェクト化してあげると良いです。
今回は外部APIとの連携部分をモックオブジェクト化するdouble
の利用方法について紹介します。
目次
テストコードの対象となるクラス
今回は『Qiita API経由で記事一覧を取得し、指定した単語が含まれたタイトルのみを抽出する』というメソッド(QiitaApiClient.search_item_titles(word)
)を作成しました。
今回のメソッドはあくまで説明のために作ったサンプルコードですので実用性はありません。
テストコードでは『API経由で取得した記事のタイトル検索が正常に機能しているか』を確認します。つまり、『どのように記事を取得するか』についてはテストのスコープ外となります。
lib/qiita_api_client.rb
class QiitaApiClient
class HTTPError < StandardError
def initialize(message)
super "connection failed: #{message}"
end
end
class << self
def connection
Faraday::Connection.new('https://qiita.com') do |builder|
builder.authorization :Bearer, "#{Rails.application.credentials.qiita[:token]}"
builder.request :url_encoded # リクエストパラメータを URL エンコードする
builder.response :logger # レスポンスを標準出力する
builder.adapter Faraday.default_adapter # アダプターの選択。デフォルトはNet::HTTP
builder.response :json, :content_type => "application/json" # レスポンスボディをJSONパースする
end
end
def search_item_titles(word)
response = connection.get('/api/v2/items')
item_titles = response.body.pluck('title') # レスポンスボディのうちtitleプロパティのみを抽出
item_titles.select { |title| title.include? "#{word}" } # wordが含まれるtitleのみを返す
end
end
end
下準備
テストコードを実行するための準備を行ます。
RSpecのセットアップ
今回はテスティングフレームワークにRSpecを利用します。
rspec-railsをGemfileに追加します。
Gemfile
group :development, :test do
gem 'rspec-rails'
end
gemのインストールとRSpecのセットアップを行ます。
$ bundle install
$ rails generate rspec:install
テストコードの実装
今回のテストのスコープは『API経由で取得した記事のタイトル検索が正常に機能しているか』です。
テストコードは以下のようになります。
spec/lib/qiita_api_client_spec.rb
require 'rails_helper'
describe 'QiitaApiClient' do
describe '.search_item_titles' do
let(:response_body) { [{ "title" => "hoge" }, { "title" => "fuga" }] }
before do
connection_mock = double('connection_mock')
response_mock = double('response_mock', status: 200, body: response_body)
allow(connection_mock).to receive(:get).and_return(response_mock)
allow(QiitaApiClient).to receive(:connection).and_return(connection_mock)
end
context '検索がヒットした場合' do
it 'データが取得できること' do
response = QiitaApiClient.search_item_titles('hoge')
expect(response.count).to eq 1
end
end
context '検索がヒットしない場合' do
it 'データが取得できないこと' do
response = QiitaApiClient.search_item_titles('xxx')
expect(response.count).to eq 0
end
end
end
end
上記のテストコードについて補足説明をします。
double
でモックオブジェクトを作成しています。
allow(QiitaApiClient).to receive(:connection).and_return(connection_mock)
とすることで、QiitaApiClient.connection
で設定されていたAPIの連携方法がモックオブジェクト(connection_mock
)化されます。
allow(connection_mock).to receive(:get).and_return(response_mock)
とすることで、connection_mock
がget
メソッドを受け取った場合response_mock
を返すように設定しています。
response_mock
はstatus
が200、body
がresponse_body
というモックオブジェクト(double
)です。
上記のテストコードをみて分かるように、double
を利用することでテストのスコープ外である外部APIとの連携設定が隠蔽化できます。
参考: 外部APIを利用したロジックのテストで使われる、WebMockとdoubleの違いについて
WebMockもdoubleも外部APIを利用したロジックのテストコードで利用されています。
それぞれの違いについては、外部APIとの連携方法がテストの関心にあるかどうかです。
WebMockの場合は外部APIのエンドポイントについてもテストコード内で記述されます。
一方、doubleではAPIとの接続方法がモックオブジェクトとして隠蔽されているためエンドポイントの記述は不要です。
つまり、外部APIとの連携方法をテストしたい場合はWebMock、連携方法については気にする必要がないのであればdoubleを利用します。
まとめ
- 外部APIの連携方法がテストのスコープ外の場合はdoubleを利用する
- モックオブジェクトのプロパティはdoubleの引数で設定する
- テストコードではallow()を利用してモックオブジェクトを返すようにする
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!