Testing Patterns

This page collects recipes that combine mocket and make_socket_pair in realistic test scenarios. Each recipe is a small standalone block; copy and adapt.

Code snippets assume:

#include <boost/corosio/test/mocket.hpp>
#include <boost/corosio/test/socket_pair.hpp>

namespace corosio = boost::corosio;
namespace capy = boost::capy;

Verifying Request Format

When a function under test must emit an exact byte sequence, stage it with expect() and assert on close() at the end:

corosio::io_context ioc;
auto [m, peer] = corosio::test::make_mocket_pair(ioc);

m.expect("GET /api/v1/users HTTP/1.1\r\n\r\n");

auto task = [](corosio::test::mocket& m_ref) -> capy::task<> {
    co_await my_http_get(m_ref, "/api/v1/users");
};
capy::run_async(ioc.get_executor())(task(m));
ioc.run();

auto ec = m.close();   // !ec means everything was written

Staging Server Responses

When a function under test must consume canned bytes, stage them with provide() and run the consumer:

m.provide(
    "HTTP/1.1 200 OK\r\n"
    "Content-Length: 5\r\n"
    "\r\n"
    "Hello");

auto task = [](corosio::test::mocket& m_ref) -> capy::task<> {
    auto response = co_await my_http_read(m_ref);
    // assert on parsed response
};

Simulating Chunked / Partial I/O

Most read loops are wrong because they assume read_some delivers as much as the kernel had. Force short reads to flush out that bug:

auto [m, peer] = corosio::test::make_mocket_pair(ioc, {}, /*max_read_size=*/4);

m.provide("ABCDEFGH");

auto task = [](corosio::test::mocket& m_ref) -> capy::task<> {
    std::string acc;
    char buf[16];
    for (int i = 0; i < 2; ++i)
    {
        auto [ec, n] = co_await m_ref.read_some(capy::make_buffer(buf));
        acc.append(buf, n);   // n == 4 each time
    }
};

The same applies to max_write_size for write-loop testing.

Layering Streams on a Mocket

m.socket() returns the underlying tcp_socket. Stack any stream that wraps a TCP socket on top of it; the staging buffers still apply at the TCP layer, with the higher-level stream’s wire format flowing through:

auto [m, peer] = corosio::test::make_mocket_pair(ioc);

// Pass m.socket() into a TLS stream or other layer in production code:
corosio::tcp_socket& under = m.socket();
// e.g., openssl_stream<corosio::tcp_socket*> tls(&under, tls_ctx);

This is the right tool when the bytes you want to stage are below a higher-level protocol.

End-to-End with Real Sockets

When fidelity matters more than determinism — for example, exercising shutdown() ordering, real set_option paths, or TLS handshakes — use make_socket_pair:

corosio::io_context ioc;
auto [s1, s2] = corosio::test::make_socket_pair(ioc);

auto task = [](corosio::tcp_socket& a, corosio::tcp_socket& b)
    -> capy::task<> {
    auto [wec, wn] =
        co_await a.write_some(capy::const_buffer("payload", 7));

    char buf[16] = {};
    auto [ec, n] = co_await b.read_some(capy::make_buffer(buf));
    // buf[0..n] == "payload"
};
capy::run_async(ioc.get_executor())(task(s1, s2));
ioc.run();

Deterministic Close-Verification

Always call close() on the mocket at the end of a test that uses provide / expect, and assert the result is empty:

auto ec = m.close();
// ec == capy::error::test_failure means leftover provide() data was
// never read, or expect() data was never written. Either way, the test
// would have passed silently without this check.

This is the line that catches "the test passed because the code under test silently did nothing." Treat it as a test-suite convention.