C++でネットワークプログラミングをするときは、cpp-netlib という Boost.Asio をベース
としたライブラリを使っているのですが、モックがほしいことがあります。
例えば下記のコードで、http://www.boost.org に実際にアクセスするのではなく任意のレスポンスを返したい。cpp-netlib のモックライブラリがないか探しましたが、どうやら無いようなので作成する
ことにしました。なお、cpp-netlib のバージョンは 0.11.1RC2 となります。
main.cpp
#include <iostream>
#include <boost/network/protocol/http/client.hpp>
int main() {
namespace network = boost::network;
namespace http = network::http;
using tag = http::tags::http_async_8bit_tcp_resolve;
using client_type = http::basic_client<tag,1,1>;
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 << "#response" << std::endl;
std::cout << http::status(response) << std::endl;
std::cout << http::body(response) << std::endl;
for (auto && h: http::headers(response)) {
std::cout << h.first << ": " << h.second << std::endl;
}
return 0;
}
boost::network::http::basic_client の調査
まず、http::basic_client
では、http::basic_client_facade
を継承していて、get()
などのメンバ関数はこのクラスで定義されています。
これらのメンバ関数ではネットワークアクセスの詳細は http::basic_client_impl
クラスの request_skeleton()
メンバ関数に委譲されています。
cpp-netlib/boost/network/protocol/http/client.hpp
template <class Tag, unsigned version_major, unsigned version_minor>
struct basic_client : basic_client_facade<Tag, version_major, version_minor> {
};
cpp-netlib/boost/network/protocol/http/client/facade.hpp
template <class Tag, unsigned version_major, unsigned version_minor>
struct basic_client_facade {
typedef basic_client_impl<Tag, version_major, version_minor> pimpl_type;
response get(
request const & request,
body_callback_function_type body_handler = body_callback_function_type()
) {
return pimpl->request_skeleton(
request,
"GET",
true,
body_handler,
body_generator_function_type()
);
}
protected:
boost::shared_ptr<pimpl_type> pimpl;
};
次に、basic_client_impl
クラスは http::impl::client_base::type
クラスを継承していて、Tag
テンプレート引数により特殊化して実装を分けています。 例えば、Tag
が http::tags::http_async_8bit_tcp_resolve
の場合は、http::impl::async_client
が選択されます。request_skeleton()
はこの client_base::type
クラスで定義されます。
cpp-netlib/boost/network/protocol/http/client/pimpl.hpp
namespace impl {
template <class Tag, unsigned version_major, unsigned version_minor,
class Enable = void>
struct client_base {
typedef unsupported_tag<Tag> type;
};
template <class Tag, unsigned version_major, unsigned version_minor>
struct client_base<Tag, version_major, version_minor,
typename enable_if<is_async<Tag> >::type> {
typedef async_client<Tag, version_major, version_minor> type;
};
template <class Tag, unsigned version_major, unsigned version_minor>
struct client_base<Tag, version_major, version_minor,
typename enable_if<is_sync<Tag> >::type> {
typedef sync_client<Tag, version_major, version_minor> type;
};
}
template <class Tag, unsigned version_major, unsigned version_minor>
struct basic_client_impl :
impl::client_base<Tag, version_major, version_minor>::type {
};
cpp-netlib/boost/network/protocol/http/client/async_impl.hpp
namespace impl {
template <class Tag, unsigned version_major, unsigned version_minor>
struct async_client : connection_policy<Tag, version_major, version_minor>::type {
basic_response<Tag> const request_skeleton(
basic_request<Tag> const & request_,
string_type const & method,
bool get_body,
body_callback_function_type callback,
body_generator_function_type generator
);
};
最後に、client_base
クラスを特殊化するにあたって、Tag
がどのように定義されているかを調べます。
Tag
の定義は http::tags
と tags
にあります。
これらを見ると、非同期通信かどうかTPCかどうかなどの特性を型として定義し、型リストを用いて組み合わせ、BOOST_NETWORK_DEFINE_TAG
マクロで型リストの各型をすべて継承する型を作成しているようです。
つまり、mock
という特性型を作成し、http_mock_8bit_tcp_resolve
というタグ型を作り、client_base
を特殊化し、mock_client
というクライアント型を作成すれば良さそうです。
cpp-netlib/boost/network/protocol/http/tags.hpp
struct http {
typedef mpl::true_::type is_http;
};
typedef mpl::vector<http, client, simple, async, tcp, default_string>
http_async_8bit_tcp_resolve_tags;
BOOST_NETWORK_DEFINE_TAG(http_async_8bit_tcp_resolve);
cpp-netlib/boost/network/tags.hpp
struct async {
typedef mpl::true_::type is_async;
};
template <class Tag>
struct components;
#ifndef BOOST_NETWORK_DEFINE_TAG
#define BOOST_NETWORK_DEFINE_TAG(name) \
struct name \
: mpl::inherit_linearly<name##_tags, \
mpl::inherit<mpl::placeholders::_1, \
mpl::placeholders::_2> >::type {}; \
template <> \
struct components<name> { \
typedef name##_tags type; \
};
#endif
モックの作成
まず、タグ型を定義します。
mock.hpp
namespace boost { namespace network {
namespace tags {
struct mock {
using is_mock = mpl::true_::type;
};
}
namespace http { namespace tags {
using http_mock_8bit_tcp_resolve_tags =
mpl::vector<http, client, simple, mock, tcp, default_string>;
BOOST_NETWORK_DEFINE_TAG(http_mock_8bit_tcp_resolve);
}}
}
次に、これを用いて client_base
を特殊化します。
mock.hpp
namespace boost { namespace network {
template <class Tag, class Enable = void>
struct is_mock : mpl::false_ {};
template <class Tag>
struct is_mock<Tag, typename enable_if<typename Tag::is_mock>::type> : mpl::true_ {};
namespace http { namespace impl {
template <class Tag, unsigned version_major, unsigned version_minor>
struct client_base<Tag, version_major, version_minor,
typename enable_if<is_mock<Tag>>::type>
{
using type = mock_client<Tag, version_major, version_minor>;
};
}}
}}
最後に、mock_client
を作成し、request_skeleton()
では適当なレスポンスを作成し返すようにします。
mock.hpp
namespace boost { namespace network { namespace http { namespace impl {
template <class Tag, unsigned version_major, unsigned version_minor>
struct mock_client {
using resolver_type = typename resolver<Tag>::type;
using string_type = typename string<Tag>::type;
using body_callback_function_type = function<
void (iterator_range<char const *> const &, system::error_code const &)>;
using body_generator_function_type = function<bool(string_type &)>;
mock_client(
bool cache_resolved,
bool follow_redirect,
bool always_verify_peer,
int timeout,
boost::shared_ptr<boost::asio::io_service> service,
optional<string_type> const & certificate_filename,
optional<string_type> const & verify_path,
optional<string_type> const & certificate_file,
optional<string_type> const & private_key_file
) {}
void wait_complete() {}
basic_response<Tag> const request_skeleton(
basic_request<Tag> const & request,
string_type const & method,
bool get_body,
body_callback_function_type callback,
body_generator_function_type generator
) {
std::cout << "#request" << std::endl;
std::cout << method << std::endl;
std::cout << request.uri().string() << std::endl;
for (auto && h: network::headers(request)) {
std::cout << h.first << ": " << h.second << std::endl;
}
basic_response<Tag> result;
result
<< http::status(200)
<< network::header("Content-Type","text/plane")
<< network::header("Content-Length","4")
<< network::body("test");
return result;
}
};
}}}}
これで、モックができました。実際に使用してみます。
main.cpp
#include <iostream>
#include <boost/network/protocol/http/client.hpp>
#include "mock.hpp"
int main() {
namespace network = boost::network;
namespace http = network::http;
#if 1
using tag = http::tags::http_mock_8bit_tcp_resolve;
#else
#endif
using client_type = http::basic_client<tag,1,1>;
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 << "#response" << std::endl;
std::cout << http::status(response) << std::endl;
std::cout << http::body(response) << std::endl;
for (auto && h: http::headers(response)) {
std::cout << h.first << ": " << h.second << std::endl;
}
return 0;
}
実行結果
$ ./main
#request
GET
http://www.boost.org
Connection: close
#response
200
test
Content-Length: 4
Content-Type: text/plane
どうやらうまくいったようです。あとは任意のレスポンスを注入する機能を実装すればOKです。