C++ で HTTP 通信をするときは、cpp-netlib という、Boost.Asio ベースのライブラリを使用しているのですが、Ruby の HTTP モックライブラリである WebMock のようなものが欲しくなりました。つまり、以下のような要件を満たすものです。
- モックオブジェクトに簡単に入れ替えることができる
- レスポンスをあらかじめ設定しておくことで、希望する状況を簡単に作り出せる
- どのようなリクエストがあったか調べることができる
これは、API を使用するときに特に欲しいものです。そこで、このようなライブラリが既にないか調べましたが、満足のいくものは見つかりませんでした。そのため、自分で作ることになり、cpp-webmock というライブラリを作成しました。本エントリーでは、バージョン 0.1.0 について述べます。
cpp-webmock とは何か?
cpp-webmock とは上記で述べた要件を満たすため、Ruby の WebMock を参考にして作られたものです。たとえば、以下のように使用します。
まず、cpp-netlib を利用した以下のコードがあったとします。
#include <iostream>
#include <boost/network/protocol/http/client.hpp>
int main() {
namespace network = boost::network;
namespace http = network::http;
using client_type = http::client;
client_type::request request("http://www.boost.org/");
request << network::header("Connection","Close");
client_type client;
client_type::response response = client.get(request);
std::cout << "status: " << http::status(response) << std::endl;
std::cout << "body: " << http::body(response) << std::endl;
return 0;
}
これをビルドし、実行すると次が出力されます。
status: 200
body: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta name="generator" content=
"HTML Tidy for Windows (vers 1st November 2003), see www.w3.org" />
<title>Boost C++ Libraries</title>
...
これに、以下の手順を元に cpp-webmock を導入します。
-
webmock/api.hpp
とwebmock/adapter/cpp_netlib.hpp
をインクルード - クライアントを
boost::network::http::client
からwebmock::adapter::cpp_netlib::client
に変更 -
a_stub
で指定URLにアクセスがあったときの振る舞いを設定 - 指定URLにアクセスがあったかどうかを調べるためには
a_request
を使用する
#include <iostream>
#include <boost/network/protocol/http/client.hpp>
#include <webmock/api.hpp> // 1.
#include <webmock/adapter/cpp_netlib.hpp> // 1.
int main() {
namespace network = boost::network;
namespace http = network::http;
using client_type = webmock::adapter::cpp_netlib::client; // 2.
using namespace webmock::api::directive; // 3.
a_stub("http://www.boost.org/").returns(a_response().status("200").body("test")); // 3.
client_type::request request("http://www.boost.org/");
request << network::header("Connection","Close");
client_type client;
client_type::response response = client.get(request);
std::cout << "status: " << http::status(response) << std::endl;
std::cout << "body: " << http::body(response) << std::endl;
std::cout << "access count: " // 4.
<< a_request("http://www.boost.org/").conditions(with_header("Connection","Close")).count() // 4.
<< std::endl; // 4.
return 0;
}
実行してみます。
status: 200
body: test
access count: 1
レスポンスが指定したものに置き換えられています。
cpp-webmock のインストール
cpp-webmock はヘッダーオンリーなライブラリなので、ビルドする必要はありません。使用するには、以下が必要です。
- Boost 1.56
- cpp-netlib 0.11.1RC2
- clang 3.5 (c++14)
ビルドに CMake を使用しているのなら、以下のようにするとよいでしょう。
cmake_minimum_required(VERSION 3.0.2)
find_package(Boost 1.56 REQUIRED system thread)
add_compile_options(-std=gnu++14 -stdlib=libc++)
include_directories(
${Boost_INCLUDE_DIRS}
/path/to/cpp-netlib
/path/to/cpp-webmock
)
link_directories(
/path/to/cpp-netlib/libs/network/src
)
set(libs
${Boost_LIBRARIES}
cppnetlib-uri
cppnetlib-client-connections
)
# cpp-netlib で SSL 通信がしたい場合
find_package(OpenSSL REQUIRED)
include_directories(${OPENSSL_INCLUDE_DIR})
list(APPEND libs ${OPENSSL_LIBRARIES})
add_definitions(-DBOOST_NETWORK_ENABLE_HTTPS)
add_executable(bin src.cpp)
target_link_libraries(bin ${libs})
cpp-webmock の機能
さて、ここでは cpp-webmock の機能について紹介していきます。
webmock::request
webmock::request
とは、クライアントのリクエスト内容を格納するための構造体です。
struct request {
using header_type = std::multimap<std::string, std::string>;
std::string method;
std::string url;
header_type headers;
std::string body;
};
比較演算子、ストリーム演算子が定義されています。
webmock::response
webmock::response
とは、クライアントに返すレスポンスを格納するための構造体です。
struct response {
using header_type = std::multimap<std::string, std::string>;
std::string status;
std::string body;
header_type headers;
};
比較演算子、ストリーム演算子が定義されています。
webmock::api
webmock::api
では、a_stub
やa_request
、a_response
といった、レスポンスの設定と検証のための機能が定義されています。これらには、以下の種類があります。
a_stub
指定されたURLアクセスがあった時のレスポンスを設定します。以下のようなバリエーションがあります。
a_stub()
a_stub(<url>)
a_stub(<method>, <url>)
<method>
にはHTTPメソッド、<url>
にはURLを指定します。
a_stub
で設定されたスタブは、スタブリストの先頭に挿入されます。そして、クライアントがアクセスしたときは、リクエストにマッチするスタブがスタブリストの先頭から線形探索されます。このため、リクエストにマッチするスタブが複数あった場合は、後に設定されたものが選択されます。
a_stub("http://www.boost.org/").returns(a_response({"200"}).body("response 1"));
a_stub("http://www.boost.org/").returns(a_response({"200"}).body("response 2"));
client_type::request request("http://www.boost.org/");
client_type client;
client_type::response response = client.get(request);
std::cout << http::body(response) << std::endl; // response 2
以下、a_stub
に定義されているメソッドです。
a_stub::returns(<response-sequence>[, ...])
a_stub::returns()
では、どのようなレスポンスを返すのかを設定します。これには複数指定でき、アクセスのたびに指定した順序で返されます。指定したレスポンス以上のアクセスがあると、一番最後に設定したレスポンスを返し続けます。
a_stub(url)
.returns(a_response({"200"}).body("response 1"))
.returns(a_response({"200"}).body("response 2"))
.returns(a_response({"200"}).body("response 3"));
たとえば、このように設定してあった場合、クライアントがurl
にアクセスしたときに返されるレスポンスは以下のようになるでしょう。
response 1
response 2
response 3
response 3
...
また、以下のように一度に複数設定することもできます。
a_stub(url).returns(
a_response({"200"}).body("response 1"),
a_response({"200"}).body("response 2"),
a_response({"200"}).body("response 3")
);
a_stub::conditions(<condition>[, ...])
a_stub::conditions()
では、HTTPヘッダーの有無や、リクエストボディの条件など、リクエストの条件を追加します。この条件には、with_method
やwith_body
などがあります。以下は、HTTPメソッドがGET
で、リクエストボディがtest
である時に、400
を返します。
a_stub()
.conditions(with_method("GET"))
.conditions(with_body("test"))
.returns(a_response().status("400"));
a_stub::conditions()
は、a_stub::returns()
と同様に、一度に複数設定することができます。
a_stub().conditions(
with_method("GET"),
with_body("test")
).returns(a_response().status("400"));
operator <<(<a_stub>, <response-sequence|condition>)
上記で、レスポンスの指定にstub::returns()
メソッドを、条件の追加にstub::conditions()
メソッドを追加すると述べましたが、<<
演算子を使用することもできます。
a_stub()
<< with_method("GET")
<< with_body("test")
<< a_response().status("400");
a_stub::count()
a_stub::count()
は、レスポンスを返した回数――つまり、リクエストされた回数を返します。
auto && stub = a_stub("http://www.hogebar.com/").returns(a_response().status("200"));
client_type client;
client.get(client_type::request{"http://www.hogebar.com/"});
client.get(client_type::request{"http://www.hogebar.com/"});
std::cout << stub.count() << std::endl; // 2
operator std::size_t()
整数型への変換関数です。変換されたあとの値はa_stub::count()
で得られる値と同じです。
std::cout << std::boolalpha << (stub < 3) << std::endl; // true
a_request
a_request
は、クライアントがどのようなリクエストを行ったかを検証するためのもので、a_stub
から、レスポンス生成機能と、スタブリストへの登録機能を取り除いたものです。複数のスタブへのリクエストを検証するときに使います。
a_stub("http://www.hogebar.com/a").returns(a_response().status("200"));
a_stub("http://www.hogebar.com/b").returns(a_response().status("200"));
client_type client;
client.get(client_type::request{"http://www.hogebar.com/a"});
client.get(client_type::request{"http://www.hogebar.com/b"});
std::cout << a_request(std::regex("http://www.hogebar.com/.*")).count() << std::endl; // 2
a_response
stub::returns()
に指定するレスポンスを生成します。これには以下のバリエーションがあります。
a_response()
a_response(<response>)
a_response(<unary-function>)
1 および 2 の形式では、リクエストの内容によらない静的なレスポンスを生成します。<response>
には、レスポンスの内容となるwebmock::response
を指定します。
a_response({"200", "test", {
{"Content-Type", "text/plane"},
{"Set-Cookie", "a=1"},
{"Set-Cookie", "b=2"}
}});
また、以下のメソッドでも設定できます。
static_response::status(<value>)
static_response::body(<value>)
static_response::header(<header-name>, <value>)
a_response().status("200").body("test")
.header("Content-Type", "text/plane")
.header("Set-Cookie", "a=1")
.header("Set-Cookie", "b=2");
3 の形式では、リクエストに応じたレスポンスを生成します。<unary-function>
には、クライアントからのリクエストwebmock::request
を引数にとる単項関数を指定します。
a_response([](webmock::request const & request){
webmock::response response;
response.status = "200";
response.body = request.body;
return response;
});
この形式では、上記で示したstatus(<value>)
などのメソッドは利用できません。
また、いずれの形式もtimes()
メソッド、もしくは*
演算子を使って、複数回、同じレスポンスを返せます。
a_stub(url)
<< a_response({"200"}).body("response 1").times(2)
<< a_response({"200"}).body("response 2") * 2
<< a_response({"200"}).body("response 3");
response 1
response 1
response 2
response 2
response 3
response 3
...
an_error
an_error
は、a_response
と同じく、a_stub::returns()
に指定しますが、こちらはレスポンスを返すのではなく、例外を投げます。名前解決できなかった場合(cpp-netlib は名前解決ができないと例外を投げる)などを再現したい時に使用します。これには、以下のバリエーションがあります。
an_error(<value>)
an_error<Exception>(<arg1>[, ...])
1 の形式では、値<value>
を投げますが、2 の形式では、例外クラスException
のインスタンスを<arg1> ...
で生成して投げます。また、いずれの形式もa_response
と同様にtimes()
メソッド、もしくは*
演算子を使って、複数回、繰り返せます。
a_stub(url)
<< an_error("Host not found (authoritative)") * 2
<< an_error<std::logic_error>("Host not found (authoritative)");
with_method
a_stub::conditions()
に指定する条件で、HTTPメソッドを指定します。これには以下のバリエーションがあります。
with_method(<value>)
with_method(<regex>)
1 の形式は、指定した値<value>
に等しいかどうか、2 の形式では指定した正規表現にマッチするかを見ます。サポートしている正規表現型はstd::regex
のみです。また、a_stub(<method>,<url>)
の<method>
は、これと同じです。
with_url
a_stub::conditions()
に指定する条件で、URLを指定します。これには以下のバリエーションがあり、それぞれwith_method
と同様です。また、a_stub(<url>)
・a_stub(<method>,<url>)
の<url>
は、これと同じです。
with_url(<value>)
with_url(<regex>)
with_body
a_stub::conditions()
に指定する条件で、リクエストボディを指定します。これには以下のバリエーションがあり、それぞれwith_method
と同様です。
with_body(<value>)
with_body(<regex>)
with_header
a_stub::conditions()
に指定する条件で、HTTPヘッダを指定します。これには以下のバリエーションがあります。
with_header(<header-name>, <value>)
with_header(<header-name>, <regex>)
1 の形式は、ヘッダ名<header-name>
の各値が、指定した値<value>
とすべて等しいかどうか、2 の形式は、ヘッダ名<header-name>
の各値が、指定した正規表現<regex>
にすべてマッチするかどうかを見ます。
with
with(<unary-function>)
a_stub::conditions()
に指定する条件で、リクエストの条件を細かくチェックしたいときに使用します。<unary-function>
には、クライアントからのリクエストwebmock::request
を引数にとる単項関数を指定します。
a_stub(url).conditions(with([](webmock::request const & request){
using namespace boost::property_tree;
ptree json;
std::istringstream iss(request.body);
json_parser::read_json(iss, json);
if (auto && id = json.get_optional<int>("user.id")) {
if (*id >= 100) return true;
}
return false;
}));
また、この場合はwith
を使わずに、直接書く方法もあります。
a_stub(url).conditions([](webmock::request const & request){
...
});
webmock::adapter::cpp_netlib
cpp_netlib のクライアントboost::network::http::basic_client
を置き換えるためのbasic_client
、そして、それを切り替えるためのヘルパーメタ関数select_by_type
・select_by_param
が定義されています。
basic_client
boost::network::http::basic_client
の代替となるものです。テンプレートになっており、テンプレートを指定したcpp-netlibのbasic_client
型を指定します。
using namespace boost::network;
using namespace webmock::adapter;
using original_client_type = http::basic_client<http::tags::http_default_8bit_tcp_resolve,1,1>;
using client_type = cpp_netlib::basic_client<original_client_type>;
また、型にboost::network::http::client
を指定したものへのエイリアスwebmock::adapter::cpp_netlib::client
が定義されています。
select_by_type
通常、cpp-netlib のbasic_client
と cpp-webmock のbasic_client
を切り替えるのには、下記のようなマクロを用います。
#include <boost/network/protocol/http/client.hpp>
#define CPP_NETLIB_CLIENT boost::network::http::client
#ifndef USE_WEBMOCK
using client_type = CPP_NETLIB_CLIENT;
#else
#include <webmock/adapter/cpp_netlib.hpp>
using client_type = webmock::adapter::cpp_netlib::basic_client<CPP_NETLIB_CLIENT>;
#endif
#undef CPP_NETLIB_CLIENT
int main() {
client_type client;
...
return 0;
}
しかし、ちょっとした用途で切り替えたい場合に、いちいちマクロを書くのはめんどうです。select_by_type
メタ関数を用いれば、簡単に切り替えることができます。
#include <boost/network/protocol/http/client.hpp>
#include <webmock/adapter/cpp_netlib.hpp>
int main() {
namespace http = boost::network::http;
namespace webmock_adapter = webmock::adapter::cpp_netlib;
using client_type = typename webmock_adapter::select_by_type<USE_WEBMOCK, http::client>::type;
client_type client;
...
return 0;
}
ここで、第1テンプレートパラメータには、切り替えるかどうかの値を指定します。true
に評価される値を指定すると cpp-webmock で提供されているクライアント型を、false
に評価される値を指定すると、cpp-netlib のクライアント型を返します。第2テンプレートパラメータは、ベースとなる cpp-netlib のクライアント型です。
select_by_param
select_by_type
と同じようにクライアント型の切り替えを行います。こちらでは、テンプレートパラメータを指定します。
using client_type = typename webmock_adapter::select_by_param<USE_WEBMOCK,
http::tags::http_default_8bit_tcp_resolve, 1, 1
>::type;
cpp-webmock の課題点
いまのところ下記の課題点があります。
-
a_response
- ステータスを指定しないと、HTTPステータスが
0
になってしまう。- 指定しない場合は
200
に設定する
- 指定しない場合は
- ステータスを指定しないと、HTTPステータスが
-
with_method
- 大文字・小文字を区別してしまう
-
with_header
- ヘッダー名が大文字小文字を区別してしまう
-
with_url
- 同一URLと解釈される場合でもマッチしない場合がある
-
http://www.hoge.jp:80
とhttp://www.hoge.jp
-
http://www.hoge.jp/
とhttp://www.hoge.jp
-
- 同一URLと解釈される場合でもマッチしない場合がある
- クライアントからのリクエストにマッチする
a_stub
で設定したスタブがない場合、空のレスポンスが返される- 例外を投げる
- アサーションのために投げる例外を設定できるようにする
- スタブがない場合は、通常どうりに外部にHTTPアクセスするオプションを追加する
- 例外を投げる
- cpp-netlib のアダプターは、基本的なケースしか想定されていない
- io_service などを駆使している場合などは動くかわからない
- おそらくスレッドセーフではない(確認していない)
- おそらく clang 3.5、c++14 にしか対応していない(確認していない)
- ほかの HTTP クライアントライブラリに対応する
- cpp-netlib に依存した設計ではないので、Poco など、ほかのクライアントライブラリにも対応させる
明日は、@wx257osn2 さんです。