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:
|
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()orexpect(). -
Designed for single-threaded, deterministic testing.
Limitations
-
provide/expectare 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
-
Socket Pairs — when you need real socket semantics instead of staged bytes.
-
Testing Patterns — recipes that combine mocket, socket_pair, and chunked I/O.
-
Sockets Guide — the underlying socket interface.
-
Error Handling — testing error paths.