Mock Sockets

The mocket class provides mock sockets for testing I/O code without actual network operations. Mockets let you stage data for reading and verify expected writes.

Code snippets assume:
#include <boost/corosio/test/mocket.hpp>
#include <boost/capy/test/fuse.hpp>

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

Overview

Mockets are testable socket-like objects:

// Create connected pair
capy::test::fuse f;
auto [client, server] = corosio::test::make_mockets(ioc, f);

// Stage data on server for client to read
server.provide("Hello from server");

// Stage expected data that client should write
client.expect("Hello from client");

// Now run your code that uses client/server as io_stream&

Creating Mockets

Mockets are created in connected pairs:

corosio::io_context ioc;
capy::test::fuse f;

auto [m1, m2] = corosio::test::make_mockets(ioc, f);

The pair is connected via loopback TCP sockets. Data written to one can be read from the other, plus you can use the staging/expectation API.

Staging Data for Reads

Use provide() to stage data that the peer will read:

// On server: stage data for client to read
server.provide("HTTP/1.1 200 OK\r\n\r\nHello");

// Now when client reads, it gets this data
auto [ec, n] = co_await client.read_some(buffer);
// buffer contains "HTTP/1.1 200 OK\r\n\r\nHello"

Multiple provide() calls append data:

server.provide("Part 1");
server.provide("Part 2");
// Client sees "Part 1Part 2"

Setting Write Expectations

Use expect() to verify what the caller writes:

// Client should send this exact data
client.expect("GET / HTTP/1.1\r\n\r\n");

// Now client writes
co_await corosio::write(client, request_buffer);

// If written data doesn't match, fuse fails

How Matching Works

When you write to a mocket with expectations:

  1. Written data is compared against the expect buffer

  2. If it matches, the expect buffer is consumed

  3. If it doesn’t match, fuse.fail() is called

After the expect buffer is exhausted, writes pass through to the real socket.

Closing and Verification

Use close() to verify all expectations were met:

auto ec = client.close();
if (ec)
    std::cerr << "Test failed: " << ec.message() << "\n";

The close() method:

  1. Closes the underlying socket

  2. Checks that provide() buffer is empty (all data read)

  3. Checks that expect() buffer is empty (all expected data written)

  4. Returns error and calls fuse.fail() if verification fails

The Fuse

Mockets work with capy::test::fuse for error injection:

capy::test::fuse f;
auto [m1, m2] = corosio::test::make_mockets(ioc, f);

// The first mocket (m1) calls f.maybe_fail() on operations
// This enables systematic error injection testing

The second mocket (m2) doesn’t call maybe_fail(), allowing asymmetric testing.

Complete Example

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

capy::task<void> test_http_client()
{
    corosio::io_context ioc;
    capy::test::fuse f;

    auto [client, server] = corosio::test::make_mockets(ioc, f);

    // Client should send this request
    client.expect(
        "GET / HTTP/1.1\r\n"
        "Host: example.com\r\n"
        "\r\n");

    // Server will respond with this
    server.provide(
        "HTTP/1.1 200 OK\r\n"
        "Content-Length: 5\r\n"
        "\r\n"
        "Hello");

    // Run the code under test
    co_await my_http_get(client, "example.com", "/");

    // Verify expectations
    auto ec1 = client.close();
    auto ec2 = server.close();

    if (ec1 || ec2)
        throw std::runtime_error("Test failed");
}

Testing with io_stream Reference

Since mocket inherits from io_stream, you can pass it to code expecting streams:

// Your production code
capy::task<void> send_message(corosio::io_stream& stream, std::string msg)
{
    co_await corosio::write(
        stream, capy::const_buffer(msg.data(), msg.size()));
}

// Test code
capy::task<void> test_send_message()
{
    auto [client, server] = make_mockets(ioc, f);

    client.expect("Hello, World!");

    co_await send_message(client, "Hello, World!");

    auto ec = client.close();
    assert(!ec);
}

Thread Safety

Mockets are NOT thread-safe:

  • Use from a single thread only

  • All coroutines must be suspended when calling expect() or provide()

  • Designed for single-threaded, deterministic testing

Limitations

  • Data staging is one-way (provide on one side, read on the other)

  • No simulation of partial writes or network delays

  • Connection errors must be injected via fuse

Use Cases

Unit Testing Protocol Code

// Test that your protocol parser handles responses correctly
server.provide("200 OK\r\nContent-Type: text/html\r\n\r\n<html>...</html>");
co_await my_protocol_read(client);
// Verify parsed result

Verifying Request Format

// Ensure your code sends correctly formatted requests
client.expect("POST /api/v1/users HTTP/1.1\r\n...");
co_await my_api_call(client, user_data);

Integration Testing Without Network

// Test client-server interaction without actual networking
server.provide(server_response);
client.expect(client_request);

co_await run_client(client);
co_await run_server(server);

Next Steps