LoginSignup
8
8

More than 5 years have passed since last update.

cpp-netlib のモックライブラリを作ってみた

Last updated at Posted at 2014-12-20

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 を導入します。

  1. webmock/api.hppwebmock/adapter/cpp_netlib.hppをインクルード
  2. クライアントをboost::network::http::clientからwebmock::adapter::cpp_netlib::clientに変更
  3. a_stubで指定URLにアクセスがあったときの振る舞いを設定
  4. 指定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_stuba_requesta_responseといった、レスポンスの設定と検証のための機能が定義されています。これらには、以下の種類があります。

a_stub

指定されたURLアクセスがあった時のレスポンスを設定します。以下のようなバリエーションがあります。

  1. a_stub()
  2. a_stub(<url>)
  3. 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_methodwith_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()に指定するレスポンスを生成します。これには以下のバリエーションがあります。

  1. a_response()
  2. a_response(<response>)
  3. 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 は名前解決ができないと例外を投げる)などを再現したい時に使用します。これには、以下のバリエーションがあります。

  1. an_error(<value>)
  2. 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メソッドを指定します。これには以下のバリエーションがあります。

  1. with_method(<value>)
  2. 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>は、これと同じです。

  1. with_url(<value>)
  2. with_url(<regex>)

with_body

a_stub::conditions()に指定する条件で、リクエストボディを指定します。これには以下のバリエーションがあり、それぞれwith_methodと同様です。

  1. with_body(<value>)
  2. with_body(<regex>)

with_header

a_stub::conditions()に指定する条件で、HTTPヘッダを指定します。これには以下のバリエーションがあります。

  1. with_header(<header-name>, <value>)
  2. 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_typeselect_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に設定する
  • with_method
    • 大文字・小文字を区別してしまう
  • with_header
    • ヘッダー名が大文字小文字を区別してしまう
  • with_url
    • 同一URLと解釈される場合でもマッチしない場合がある
      • http://www.hoge.jp:80http://www.hoge.jp
      • http://www.hoge.jp/http://www.hoge.jp
  • クライアントからのリクエストにマッチするa_stubで設定したスタブがない場合、空のレスポンスが返される
    • 例外を投げる
      • アサーションのために投げる例外を設定できるようにする
    • スタブがない場合は、通常どうりに外部にHTTPアクセスするオプションを追加する
  • cpp-netlib のアダプターは、基本的なケースしか想定されていない
    • io_service などを駆使している場合などは動くかわからない
  • おそらくスレッドセーフではない(確認していない)
  • おそらく clang 3.5、c++14 にしか対応していない(確認していない)
  • ほかの HTTP クライアントライブラリに対応する
    • cpp-netlib に依存した設計ではないので、Poco など、ほかのクライアントライブラリにも対応させる

明日は、@wx257osn2 さんです。

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8