前回のエントリーで cpp-netlib の client をモックにすげ替えることに成功しましたが、 任意のレスポンスを返すためにはどのようしたらいいか、という点は述べていませんでした。 このモックは、Ruby の WebMock のような設計にしようと考えているので、 WebMock がどのような設計になっているのかを調査しました。なお、調査した WebMock のバージョンは 1.20.4 となります。
WebMock 概要
まず、WebMock を用いて http://www.example.com/ にGETメソッドでアクセスした場合に、メッセージボディが "test"
であるレスポンスを返すためには以下のようにします。
require 'net/http' require 'webmock' webmock::api.stub_request(:get, 'www.example.com').to_return(body: "test") client = Net::HTTP.start("www.example.com") response = client.get("/") puts response.body # test
これにより、Net::HTTP
が http://www.example.com/ にアクセスしなくなり、かわりにモックで設定したレスポンスを返すようになります。
モックオブジェクトの生成と管理
ここで、モックオブジェクトの生成を行う WebMock::API::stub_request()
メソッドは何をしているのでしょうか?
このメソッドの定義は、以下のようになっています。
module WebMock module API # ... def stub_request(method, uri) WebMock::StubRegistry.instance. register_request_stub(WebMock::RequestStub.new(method, uri)) end # ... end end
まず、WebMock::RequestStub
というモックオブジェクトを生成します。
このオブジェクトでは、to_return()
というレスポンスを設定するメソッドや、
with()
メソッドというリクエストの条件を設定するメソッドが定義されています。
また、リクエストの条件は request_pattern
プロパティに WebMock::RequestPattern
というオブジェクトが保持されています。
module WebMock class RequestStub attr_accessor :request_pattern def initialize(method, uri) @request_pattern = RequestPattern.new(method, uri) # ... end def with(params = {}, &block) @request_pattern.with(params, &block) # ... end def to_return(*response_hashes, &block) # ... end # ... end end
次に、WebMock::StubRegistry
というモックオブジェクトを管理するオブジェクトの register_request_stub()
メソッドに生成したモックオブジェクトを渡しています。
このメソッドでは渡された WebMock::RequestStub
オブジェクトを request_stubs
プロパティに追加します。request_stubs
に保持されているモックオブジェクトは必要に応じて探索されます。
module WebMock class StubRegistry include Singleton attr_accessor :request_stubs def initialize reset! end # ... def reset! self.request_stubs = [] end # ... def register_request_stub(stub) request_stubs.insert(0, stub) stub end # ... end end
モックオブジェクトの探索と実行
では Net::HTTP
がリクエストを送るときは、どのような処理になっているのでしょうか?
WebMockは require
されると、Net::HTTP
を WebMock::HttpLibAdapters::NetHttpAdapter@webMockNetHTTP
に置き換えます。そして、Net::HTTP#request()
メソッドが呼ばれた時に以下の動作をするようになります。
webmock/http_lib_adapters/net_http.rb
module WebMock module HttpLibAdapters class NetHttpAdapter < HttpLibAdapter # ... @webMockNetHTTP = Class.new(Net::HTTP) do # ... def request(request, body = nil, &block) request_signature = WebMock::NetHTTPUtility.request_signature_from_request(self, request, body) # ... if webmock_response = WebMock::StubRegistry.instance.response_for_request(request_signature) # ... elsif WebMock.net_connect_allowed?(request_signature.uri) # ... else raise WebMock::NetConnectNotAllowedError.new(request_signature) end end # ... end # ... end end end
まず、WebMock::NetHTTPUtility::request_signature_from_request()
メソッドにより WebMock::RequestSignature
オブジェクト request_signature
を生成します。このオブジェクトは、各アダプターのリクエストを正規化し保持するためのものです。
つぎに、生成した request_signature
オブジェクトを WebMock::StubRegistry#response_for_request()
メソッドに渡し、
内部で request_stub_for()
メソッドを呼び出します。
そして、request_signature
オブジェクトに一致するモックオブジェクトを request_stubs
の中から各モックオブジェクトの request_pattern
プロパティの matches?()
メソッドを用いて線形探索します。
最後に、探索したモックオブジェクトに設定されているレスポンスを返します。このとき、一致するモックオブジェクトが存在しなければ、例外を投げます。ただし、WebMock::Config#allow_net_connect
が true
の時は、実際にHTTPアクセスをします。
module WebMock class StubRegistry # ... def response_for_request(request_signature) stub = request_stub_for(request_signature) stub ? evaluate_response_for_request(stub.response, request_signature) : nil end private def request_stub_for(request_signature) (global_stubs + request_stubs).detect { |registered_request_stub| registered_request_stub.request_pattern.matches?(request_signature) } end def evaluate_response_for_request(response, request_signature) response.dup.evaluate(request_signature) end end end
WebMock::RequestPattern について
上記でモックオブジェクトの探索方法について述べましたが、具体的にはどのように request_signature
に一致しているか判定しているのでしょうか?
WebMock::RequestPattern
の定義は下記のようになっています。
module WebMock class RequestPattern attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern 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 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 end def matches?(request_signature) content_type = request_signature.headers['Content-Type'] if request_signature.headers content_type = content_type.split(';').first if content_type @method_pattern.matches?(request_signature.method) && @uri_pattern.matches?(request_signature.uri) && (@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type || "")) && (@headers_pattern.nil? || @headers_pattern.matches?(request_signature.headers)) && (@with_block.nil? || @with_block.call(request_signature)) end # ... end class MethodPattern # ... end class URIPattern # ... end class BodyPattern # ... end class HeadersPattern # ... end end
これを見ると、まずオブジェクト生成時の対象HTTPメソッド・URL、with()
メソッドで設定された条件(リクエストHTTPヘッダの有無)をもとにパターンを表すオブジェクトを生成し保持します。
そして matches?()
メソッドで、これらパターンすべてにマッチする時に一致したと判断し、true
を返します。
さいごに
というわけで、WebMock のしくみがだいたいわかったので、これを参考に設計していきます。