Mock Sockets

The mocket class is a testable wrapper around a tcp_socket that lets you stage bytes for reads and assert on bytes written, without giving up real socket semantics when you don’t need them. Mockets are the main tool for byte-level deterministic tests of corosio I/O code.

Code snippets assume:

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

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

Overview

basic_mocket<Socket> holds a Socket member and forwards read_some / write_some to it, with two pre-stages:

  • If the provide buffer has bytes, reads consume them first.

  • If the expect buffer has bytes, writes are validated against them before passing through.

Once both buffers are empty, I/O passes through to the underlying socket unchanged. The default alias is using mocket = basic_mocket<>;, which uses tcp_socket.

Creating Mockets

Mockets are created paired with a peer socket, connected via loopback:

corosio::io_context ioc;

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

make_mocket_pair returns std::pair<basic_mocket<Socket>, Socket>. The first element is the mocket (test-instrumented). The second is the peer (a plain Socket). Both are open and immediately usable.

Staging Data for Reads

Use provide() to stage bytes that the mocket itself will hand back from read_some:

m.provide("HTTP/1.1 200 OK\r\n\r\nHello");

auto task = [](corosio::test::mocket& m_ref) -> capy::task<> {
    char buf[64] = {};
    auto [ec, n] = co_await m_ref.read_some(capy::make_buffer(buf));
    // buf[0..n] == "HTTP/1.1 200 OK\r\n\r\nHello"
};

Multiple provide() calls append. Reads consume from the front of the buffer; once empty, subsequent reads pass through to the underlying socket.

Setting Write Expectations

Use expect() to declare bytes that the code under test must write next:

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

auto task = [](corosio::test::mocket& m_ref) -> capy::task<> {
    auto [ec, n] = co_await m_ref.write_some(
        capy::const_buffer("GET / HTTP/1.1\r\n\r\n", 18));
    // ec is empty; n == 18
};

write_some matches the leading bytes of the buffer sequence against the expect buffer. If they match, the matched prefix is consumed; if they don’t, write_some returns capy::error::test_failure and writes zero bytes. Once the expect buffer is empty, subsequent writes pass through.

Chunked I/O

make_mocket_pair accepts max_read_size and max_write_size to cap the bytes a single read_some / write_some will deliver. This is the right tool for forcing your code’s read/write loops to handle short transfers:

// max_read_size = 4, max_write_size = 3 force short transfers.
auto [m, peer] = corosio::test::make_mocket_pair(ioc, {}, 4, 3);

m.provide("0123456789");
m.expect("abcdef");

auto task = [](corosio::test::mocket& m_ref) -> capy::task<> {
    char buf[16] = {};
    auto [rec, rn] = co_await m_ref.read_some(capy::make_buffer(buf));
    // rn == 4 ("0123")

    auto [wec, wn] = co_await m_ref.write_some(
        capy::const_buffer("abcdef", 6));
    // wn == 3 (matched "abc")
};

A value of 0 for either size parameter throws std::logic_error from the constructor. The default is std::size_t(-1) (unlimited).

Closing and Verification

close() shuts the underlying socket and verifies that both staging buffers are empty:

auto ec = m.close();
if (ec == capy::error::test_failure)
{
    // Either provide() data was never read,
    // or expect() data was never written.
}

Always call close() at the end of a test that uses provide / expect and assert that the result is empty. This is what catches "the test passed because the code under test did nothing."

Templated over Socket

basic_mocket is a template; the default alias only specializes it for tcp_socket. For backend-specific tests (native_tcp_socket<Backend>, etc.), name the specialization explicitly:

using socket_type   = corosio::native_tcp_socket<backend>;
using acceptor_type = corosio::native_tcp_acceptor<backend>;
using mocket_type   = corosio::test::basic_mocket<socket_type>;

corosio::io_context ioc(backend);

auto [m, peer] =
    corosio::test::make_mocket_pair<socket_type, acceptor_type>(ioc);

Underlying Socket Access

socket() returns a reference to the wrapped Socket. This is how you stack other streams (TLS, framing) on top of a mocket:

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

corosio::tcp_socket& under = m.socket();
// Pass `under` into a TLS stream, a custom framing layer, etc.

See Testing Patterns for a TLS-over-mocket example.

Thread Safety

  • Use a mocket from a single thread only.

  • All coroutines using the mocket must be suspended when calling provide() or expect().

  • Designed for single-threaded, deterministic testing.

Limitations

  • provide / expect are byte sequences only; there is no out-of-band signal for "now produce EOF" or "now fail" beyond the matching semantics described above.

  • No simulation of network delay; for that, run code under a stop_token with a timer.

  • Connection-establishment errors are not simulated — the pair always comes back open.

Next Steps