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

C++ で HTTP 通信をするときは、[cpp-netlib](http://cpp-netlib.org/) という、Boost.Asio ベースのライブラリを使用しているのですが、Ruby の HTTP モックライブラリである [WebMock](https://github.com/bblimke/webmock) のようなものが欲しくなりました。つまり、以下のような要件を満たすものです。

* モックオブジェクトに簡単に入れ替えることができる
* レスポンスをあらかじめ設定しておくことで、希望する状況を簡単に作り出せる
* どのようなリクエストがあったか調べることができる

これは、API を使用するときに特に欲しいものです。そこで、このようなライブラリが既にないか調べましたが、満足のいくものは見つかりませんでした。そのため、自分で作ることになり、[cpp-webmock](https://github.com/mrk21/cpp-webmock "mrk21/cpp-webmock") というライブラリを作成しました。本エントリーでは、[バージョン 0.1.0](https://github.com/mrk21/cpp-webmock/tree/v0.1.0) について述べます。

## cpp-webmock とは何か？

cpp-webmock とは上記で述べた要件を満たすため、Ruby の WebMock を参考にして作られたものです。たとえば、以下のように使用します。

まず、cpp-netlib を利用した以下のコードがあったとします。

```cpp
#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.hpp`と`webmock/adapter/cpp_netlib.hpp`をインクルード
1. クライアントを`boost::network::http::client`から`webmock::adapter::cpp_netlib::client`に変更
1. `a_stub`で指定URLにアクセスがあったときの振る舞いを設定
1. 指定URLにアクセスがあったかどうかを調べるためには`a_request`を使用する

```cpp
#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
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`とは、クライアントのリクエスト内容を格納するための構造体です。

```cpp
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`とは、クライアントに返すレスポンスを格納するための構造体です。

```cpp
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アクセスがあった時のレスポンスを設定します。以下のようなバリエーションがあります。

1. `a_stub()`
1. `a_stub(<url>)`
1. `a_stub(<method>, <url>)`

`<method>`にはHTTPメソッド、`<url>`にはURLを指定します。

`a_stub`で設定されたスタブは、スタブリストの先頭に挿入されます。そして、クライアントがアクセスしたときは、リクエストにマッチするスタブがスタブリストの先頭から線形探索されます。このため、リクエストにマッチするスタブが複数あった場合は、後に設定されたものが選択されます。

```cpp
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()`では、どのようなレスポンスを返すのかを設定します。これには複数指定でき、アクセスのたびに指定した順序で返されます。指定したレスポンス以上のアクセスがあると、一番最後に設定したレスポンスを返し続けます。

```cpp
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
...
```

また、以下のように一度に複数設定することもできます。

```cpp
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`を返します。

```cpp
a_stub()
    .conditions(with_method("GET"))
    .conditions(with_body("test"))
    .returns(a_response().status("400"));
```

`a_stub::conditions()`は、`a_stub::returns()`と同様に、一度に複数設定することができます。

```cpp
a_stub().conditions(
    with_method("GET"),
    with_body("test")
).returns(a_response().status("400"));
```

##### `operator <<(<a_stub>, <response-sequence|condition>)`

上記で、レスポンスの指定に`stub::returns()`メソッドを、条件の追加に`stub::conditions()`メソッドを追加すると述べましたが、`<<`演算子を使用することもできます。

```cpp
a_stub()
    << with_method("GET")
    << with_body("test")
    << a_response().status("400");
```

##### `a_stub::count()`

`a_stub::count()`は、レスポンスを返した回数――つまり、リクエストされた回数を返します。

```cpp
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()`で得られる値と同じです。

```cpp
std::cout << std::boolalpha << (stub < 3) << std::endl; // true
```

#### a_request

`a_request`は、クライアントがどのようなリクエストを行ったかを検証するためのもので、`a_stub`から、レスポンス生成機能と、スタブリストへの登録機能を取り除いたものです。複数のスタブへのリクエストを検証するときに使います。

```cpp
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()`
1. `a_response(<response>)`
1. `a_response(<unary-function>)`

1 および 2 の形式では、リクエストの内容によらない静的なレスポンスを生成します。`<response>`には、レスポンスの内容となる`webmock::response`を指定します。

```cpp
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>)`

```cpp
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`を引数にとる単項関数を指定します。

```cpp
a_response([](webmock::request const & request){
    webmock::response response;
    response.status = "200";
    response.body = request.body;
    return response;
});
```

この形式では、上記で示した`status(<value>)`などのメソッドは利用できません。

また、いずれの形式も`times()`メソッド、もしくは`*`演算子を使って、複数回、同じレスポンスを返せます。

```cpp
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>)`
1. `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

```cpp
with(<unary-function>)
```

`a_stub::conditions()`に指定する条件で、リクエストの条件を細かくチェックしたいときに使用します。`<unary-function>`には、クライアントからのリクエスト`webmock::request`を引数にとる単項関数を指定します。

```cpp
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`を使わずに、直接書く方法もあります。

```cpp
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`型を指定します。

```cpp
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`を切り替えるのには、下記のようなマクロを用います。

```cpp
#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`メタ関数を用いれば、簡単に切り替えることができます。

```cpp
#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`と同じようにクライアント型の切り替えを行います。こちらでは、テンプレートパラメータを指定します。

```cpp
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:80`と`http://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 さんです。