Ruby の WebMock では、クエリ文字列も考慮して stub_request する

外部サイトへのアクセスが発生する、 Railsアプリがあるとします。

このRailsアプリに対するテストコードでは、外部サイトへのアクセスが発生しないよう、 WebMock を使うのが便利です。
bblimke/webmock: Library for stubbing and setting expectations on HTTP requests in Ruby.

 
ただ、

stub_request(:get, 'https://example.com').to_return(status: 200)

のようにモックしたところ

WebMock::NetConnectNotAllowedError: Real HTTP connections are disabled. Unregistered request: GET https://example.com?foo=bar

というエラーが発生したため、メモを残します。

 
目次

 

環境

 

原因

エラーメッセージにもある通り、WebMockではクエリ文字列も考慮して stub_request する必要があるようです。

 
例えば、RailsアプリのControllerに外部APIを呼ぶ処理があるとします*1

ここで、外部APIは以下のFake APIを使うとします。
JSONPlaceholder - Free Fake REST API

module Api
  class PostsController < ApplicationController
    def index
      # 無意味なリクエストだけど、本来は受け取ったデータを元にあれこれする
      # See https://jsonplaceholder.typicode.com/
      connection = Faraday.new(url: 'https://jsonplaceholder.typicode.com')
      _ = connection.get '/comments?postId=1'

      render json: { data: 'ok' }
    end
  end
end

 
このControllerに対し、routesにて

Rails.application.routes.draw do
  namespace 'api' do
    get 'posts', to: 'posts#index'
  end
end

と設定されていたとします。

 
そんな中、テストコードで WebMock + RSpec を使うため、

% bin/rails generate rspec:install

で生成した spec/rails_helper.rb

require 'webmock/rspec'

と設定し、 spec/request/api/posts_spec.rb

require 'rails_helper'

RSpec.describe 'Post API', type: :request do
  describe 'GET /api/posts' do
    context 'stub失敗' do
      before do
        stub_request(:get, 'https://jsonplaceholder.typicode.com/comments').to_return(status: 200)
      end

      it 'リクエストが成功すること' do
        get '/api/posts'
        expect(response).to have_http_status(200)
      end
    end
  end
end

というテストコードを書くと、冒頭のようなエラーメッセージが表示されます。

WebMock::NetConnectNotAllowedError: Real HTTP connections are disabled. Unregistered request: GET https://jsonplaceholder.typicode.com/comments?postId=1 with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'User-Agent'=>'Faraday v1.8.0'}

You can stub this request with the following snippet:

stub_request(:get, "https://jsonplaceholder.typicode.com/comments?postId=1").
  with(
    headers: {
      'Accept'=>'*/*',
      'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
      'User-Agent'=>'Faraday v1.8.0'
    }).
  to_return(status: 200, body: "", headers: {})

registered request stubs:

stub_request(:get, "https://jsonplaceholder.typicode.com/comments")

 

対応

stub_requestのURLにクエリ文字列を含める

エラーメッセージの

You can stub this request with the following snippet

の snippet をそのまま使ってもよいのですが、テストをパスさせるだけならば、 URL にクエリ文字列を含めるだけでも大丈夫です。

context 'URLにクエリ文字列を含める' do
  before do
    stub_request(:get, 'https://jsonplaceholder.typicode.com/comments?postId=1').to_return(status: 200)
  end

  it 'リクエストが成功すること' do
    get '/api/posts'
    expect(response).to have_http_status(200)
  end
end

 

stub_request + with でクエリ文字列用のハッシュを渡す

上記のように、クエリ文字列を含んだURLを stub_request に渡すのもよいのですが、クエリ文字列の整形が大変です。

公式ドキュメントによると、 with(query: {}) のような形式でクエリ文字列を渡すこともできるようです。
https://github.com/bblimke/webmock#matching-query-params-using-hash

context 'withでクエリ文字列を指定' do
  before do
    stub_request(:get, 'https://jsonplaceholder.typicode.com/comments')
      .with(
        query: {
          'postId' => 1
        }
      )
      .to_return(status: 200)
  end

  it 'リクエストが成功すること' do
    get '/api/posts'
    expect(response).to have_http_status(200)
  end
end

 

stub_request に正規表現を渡す

今回の Controller では直接Faradayを使っていました。

ただ、場合によっては、Faraday などをラップしたクライアントが存在し、そのクライアントの中で動的にクエリ文字列を生成することもあるかもしれません。

WebMockでは、 stub_request()正規表現を渡すことで、正規表現にマッチしたURLにリクエストが飛ぶ時はモックしてくれるようです。
https://github.com/bblimke/webmock#matching-uris-using-regular-expressions

context '正規表現でstub' do
  before do
    stub_request(:get, %r{jsonplaceholder.typicode.com})
      .to_return(status: 200)
  end

  it 'リクエストが成功すること' do
    get '/api/posts'
    expect(response).to have_http_status(200)
  end
end

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/rails_webmock-sample

*1:「Controllerには外部APIを呼ぶ処理を書くべきでない」などがあるかもしれませんが、今回は説明を簡単にするために書いています