【RSpec】doubleで外部API設定をモック化してロジックのテストをする方法

Ruby

外部APIを利用したメソッドのテストコードを書く際、APIとの連携方法がテストのスコープ外になることがあります。

たとえば『API経由で受け取ったデータを正しく加工できているか』をテストする場合です。
この時のテストの関心はあくまで『データを加工するロジック』であるため『どのようにデータを取得してくるのか』という部分に関しては気にする必要がありません。

上記のようなテストでは外部APIとの連携部分をモックオブジェクト化してあげると良いです。
今回は外部APIとの連携部分をモックオブジェクト化するdoubleの利用方法について紹介します。

テストコードの対象となるクラス

今回は『Qiita API経由で記事一覧を取得し、指定した単語が含まれたタイトルのみを抽出する』というメソッド(QiitaApiClient.search_item_titles(word))を作成しました。

Qiitaの記事一覧取得APIはクエリによる検索機能を提供しています。
今回のメソッドはあくまで説明のために作ったサンプルコードですので実用性はありません。

テストコードでは『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_mockgetメソッドを受け取った場合response_mockを返すように設定しています。

response_mockstatusが200、bodyresponse_bodyというモックオブジェクト(double)です。

上記のテストコードをみて分かるように、doubleを利用することでテストのスコープ外である外部APIとの連携設定が隠蔽化できます。

参考: 外部APIを利用したロジックのテストで使われる、WebMockとdoubleの違いについて

WebMockもdoubleも外部APIを利用したロジックのテストコードで利用されています。
それぞれの違いについては、外部APIとの連携方法がテストの関心にあるかどうかです。

WebMockの場合は外部APIのエンドポイントについてもテストコード内で記述されます。
一方、doubleではAPIとの接続方法がモックオブジェクトとして隠蔽されているためエンドポイントの記述は不要です。

つまり、外部APIとの連携方法をテストしたい場合はWebMock、連携方法については気にする必要がないのであればdoubleを利用します。

【RSpec】WebMockを利用して外部APIを利用したロジックのテストをする

2020年11月16日

まとめ

  • 外部APIの連携方法がテストのスコープ外の場合はdoubleを利用する
  • モックオブジェクトのプロパティはdoubleの引数で設定する
  • テストコードではallow()を利用してモックオブジェクトを返すようにする

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!