外部 API を利用するので、こういった gem を多用することになる
Koala is a Facebook library for Ruby
The Google API Client Library for Ruby
Library for stubbing and setting expectations
on HTTP requests in Ruby.
HTTP リクエストをあたかも処理されたように
レスポンスを返すことができる。
テストの用途で使う話をします
gem install webmock
# RSpec
require 'webmock/rspec'
# MiniTest
require 'webmock/minitest'
# (stubするリクエストの登録)
# https://www.example.com/ に POST メソッドでアクセスした場合に、
# メッセージボディが "abc" であるレスポンスを返す
# method_name に :any を指定すると何でも可となる
stub_request(:post, "https://www.example.com").
with(body: "abc", headers: { 'Content-Length' => 3 }).
to_return(body: "abc")
こういうテストコードをたくさん書きます。
WebMock 用のデータ作成が大変。面倒。
Record your test suite’s HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests.
データを実際のHTTP通信から記録するライブラリ
gem 'vcr'
VCR.configure do |c|
c.cassette_library_dir = 'tmp/vcr'
c.hook_into :webmock
end
# tmp/vcr/path/to/response.yml に作られる
VCR.use_cassette("path/to/response") do
response = Net::HTTP.get_response(URI('http://example.com'))
end
stub_request(:post, "https://www.example.com").
with(body: "abc", headers: { 'Content-Length' => 3 }).
to_return(body: "abc")
https://www.example.com/ に POST メソッドでアクセスした場合に、メッセージボディが “abc” であるレスポンスを返す
ここで何が起きているのかコードを簡単に追います
stub_request(:post, "https://www.example.com").
with(body: "abc", headers: { 'Content-Length' => 3 }).
to_return(body: "abc")
エントリーポイント
webmock/lib/webmock/api.rb
module WebMock
module API
extend self
def stub_request(method, uri)
WebMock::StubRegistry.instance.
register_request_stub(WebMock::RequestStub.new(method, uri))
end
alias_method :stub_http_request, :stub_request
stub_request(:post, "https://www.example.com").
with(body: "abc", headers: { 'Content-Length' => 3 }).
to_return(body: "abc")
module WebMock
class RequestStub
def initialize(method, uri)
@request_pattern = RequestPattern.new(method, uri)
@responses_sequences = []
self
end
def with(params = {}, &block)
def to_return(*response_hashes, &block)
module WebMock
class RequestPattern
def initialize(method, uri, options = {})
@method_pattern = MethodPattern.new(method)
@uri_pattern = create_uri_pattern(uri)
@body_pattern = nil
@headers_pattern = nil
@with_block = nil
assign_options(options)
end
class MethodPattern
def initialize(pattern)
@pattern = pattern
end
def matches?(method)
@pattern == method || @pattern == :any
end
これで stub_request による登録が完了
module WebMock
module API
def stub_request(method, uri)
WebMock::StubRegistry.instance.
register_request_stub(WebMock::RequestStub.new(method, uri))
end
次は with について
stub_request(:post, "https://www.example.com").
with(body: "abc", headers: { 'Content-Length' => 3 }).
to_return(body: "abc")
register_request_stub は WebMock::RequestStub を返すので、
WebMock::RequestStub.with を見ます
module WebMock
class StubRegistry
def register_request_stub(stub)
request_stubs.insert(0, stub)
stub
end
module WebMock
class RequestStub
def with(params = {}, &block)
@request_pattern.with(params, &block)
self
end
RequestPattern#with で、
body や headers や uri pattern を保持します。
module WebMock
class RequestPattern
def with(options = {}, &block)
raise ArgumentError.new('#with method invoked with no arguments. Either options hash or block must be specified.') if options.empty? && !block_given?
assign_options(options)
@with_block = block
self
def assign_options(options)
options = WebMock::Util::HashKeysStringifier.stringify_keys!(options, deep: true)
HashValidator.new(options).validate_keys('body', 'headers', 'query', 'basic_auth')
set_basic_auth_as_headers!(options)
@body_pattern = BodyPattern.new(options['body']) if options.has_key?('body')
@headers_pattern = HeadersPattern.new(options['headers']) if options.has_key?('headers')
@uri_pattern.add_query_params(options['query']) if options.has_key?('query')
最後に to_return について
stub_request(:post, "https://www.example.com").
with(body: "abc", headers: { 'Content-Length' => 3 }).
to_return(body: "abc")
with で self を返すので同様に
WebMock::RequestPattern#to_return です
よしなに responses_sequences に保持します
module WebMock
class RequestPattern
def to_return(*response_hashes, &block)
if block
@responses_sequences << ResponsesSequence.new([ResponseFactory.response_for(block)])
else
@responses_sequences << ResponsesSequence.new([*response_hashes].flatten.map {|r| ResponseFactory.response_for(r)})
end
self
end
alias_method :and_return, :to_return
lib/webmock/http_lib_adapters の中にアダプターがあります
Net::HTTP での前提で続きを追います
Net::HTTP を @webMockNetHTTP に書き換えています。
lib/webmock/rspec.rb
require 'webmock/rspec'
すると WebMock.enable!
されます
module WebMock
module HttpLibAdapters
class NetHttpAdapter < HttpLibAdapter
adapter_for :net_http
def self.enable!
Net.send(:remove_const, :BufferedIO)
Net.send(:remove_const, :HTTP)
Net.send(:remove_const, :HTTPSession)
Net.send(:const_set, :HTTP, @webMockNetHTTP)
Net.send(:const_set, :HTTPSession, @webMockNetHTTP)
Net.send(:const_set, :BufferedIO, Net::WebMockNetBufferedIO)
end
@webMockNetHTTP = Class.new(Net::HTTP) do
def request(request, body = nil, &block)
request_signature = WebMock::NetHTTPUtility.request_signature_from_request(self, request, body)
# ここで stub RequestPattern にマッチする stub を探して評価して結果を返している
if webmock_response = WebMock::StubRegistry.instance.response_for_request(request_signature)
build_net_http_response(webmock_response, &block)