cpp-netlib にモックがほしい

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 テンプレート引数により特殊化して実装を分けています。 例えば、Taghttp::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::tagstags にあります。 これらを見ると、非同期通信かどうか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;

// Tag Definition Macro Helper
#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  // BOOST_NETWORK_DEFINE_TAG
// ...

モックの作成

まず、タグ型を定義します。

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
    using tag = http::tags::http_async_8bit_tcp_resolve;
#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です。