Mock Stream Testing
Unit testing protocol code with mock streams and error injection.
What You Will Learn
-
Using
test::read_streamandtest::write_stream -
Error injection with
fuse -
Synchronous testing with
run_blocking
Prerequisites
-
Completed Buffer Composition
-
Understanding of streams from Streams
Source Code
#include <boost/capy.hpp>
#include <boost/capy/test/stream.hpp>
#include <boost/capy/test/fuse.hpp>
#include <boost/capy/io/any_stream.hpp>
#include <boost/capy/cond.hpp>
#include <iostream>
#include <cassert>
#include <cctype>
using namespace boost::capy;
// A simple protocol: read until newline, echo back uppercase
// Takes any_stream& so the function is transport-independent
task<bool> echo_line_uppercase(any_stream& stream)
{
std::string line;
char c;
// Read character by character until newline
while (true)
{
// ec: std::error_code, n: std::size_t
auto [ec, n] = co_await stream.read_some(mutable_buffer(&c, 1));
if (ec)
{
if (ec == cond::eof)
break;
co_return false;
}
if (c == '\n')
break;
line += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
line += '\n';
// Echo uppercase - loop until all bytes written
std::size_t written = 0; // std::size_t - total bytes written
while (written < line.size())
{
// wec: std::error_code, wn: std::size_t
auto [wec, wn] = co_await stream.write_some(
const_buffer(line.data() + written, line.size() - written));
if (wec)
co_return false;
written += wn;
}
co_return true;
}
void test_happy_path()
{
std::cout << "Test: happy path\n";
// Use fuse in disarmed mode (no error injection) for happy path
test::fuse f; // test::fuse
test::stream mock(f); // test::stream
mock.provide("hello\n");
// Wrap mock in any_stream using pointer construction for reference semantics
any_stream stream{&mock}; // any_stream
bool result = false; // bool
test::run_blocking([&](bool r) { result = r; })(echo_line_uppercase(stream));
assert(result == true);
assert(mock.data() == "HELLO\n");
std::cout << " PASSED\n";
}
void test_partial_reads()
{
std::cout << "Test: partial reads (1 byte at a time)\n";
// Use fuse in disarmed mode (no error injection)
test::fuse f; // test::fuse
// Mock returns at most 1 byte per read_some
test::stream mock(f, 1); // test::stream, max_read_size = 1
mock.provide("hi\n");
// Wrap mock in any_stream using pointer construction for reference semantics
any_stream stream{&mock}; // any_stream
bool result = false; // bool
test::run_blocking([&](bool r) { result = r; })(echo_line_uppercase(stream));
assert(result == true);
assert(mock.data() == "HI\n");
std::cout << " PASSED\n";
}
void test_with_error_injection()
{
std::cout << "Test: error injection\n";
int success_count = 0;
int error_count = 0;
// fuse::armed runs the test repeatedly, failing at each
// operation point until all paths are covered
test::fuse f; // test::fuse
auto r = f.armed([&](test::fuse&) -> task<> { // fuse::result
test::stream mock(f); // test::stream
mock.provide("test\n");
// Wrap mock in any_stream using pointer construction for reference semantics
any_stream stream{&mock}; // any_stream
// Run the protocol - fuse will inject errors at each step
bool result = co_await echo_line_uppercase(stream); // bool
// Either succeeds with correct output, or fails cleanly
if (result)
{
++success_count;
assert(mock.data() == "TEST\n");
}
else
{
++error_count;
}
});
// Verify that fuse testing exercised both paths
std::cout << " Runs: " << (success_count + error_count)
<< " (success=" << success_count
<< ", error=" << error_count << ")\n";
assert(r.success);
assert(success_count > 0); // At least one successful run
assert(error_count > 0); // At least one error-injected run
std::cout << " PASSED (all error paths tested)\n";
}
int main()
{
test_happy_path();
test_partial_reads();
test_with_error_injection();
std::cout << "\nAll tests passed!\n";
return 0;
}
Build
add_executable(mock_stream_testing mock_stream_testing.cpp)
target_link_libraries(mock_stream_testing PRIVATE capy)
Walkthrough
Mock Streams
test::fuse f; // test::fuse
test::stream mock(f); // test::stream
mock.provide("hello\n");
test::stream is a bidirectional mock that satisfies both ReadStream and WriteStream:
-
Constructor takes a
fuse&for error injection -
provide(data)— Supplies data for reads -
data()— Returns data written to the mock -
Second constructor parameter controls max bytes per operation
Type-Erased Streams
// Wrap mock in any_stream using pointer construction for reference semantics
any_stream stream{&mock}; // any_stream
Use pointer construction (&mock) so the any_stream wrapper references the mock without taking ownership. This allows inspecting mock.data() after operations.
Synchronous Testing
bool result = false; // bool
test::run_blocking([&](bool r) { result = r; })(echo_line_uppercase(stream));
run_blocking executes a coroutine synchronously, blocking until complete. Pass a handler to capture the result.
Error Injection
test::fuse f; // test::fuse
auto r = f.armed([&](test::fuse&) -> task<> {
test::stream mock(f); // test::stream
// ... run test ...
});
fuse::armed runs the test function repeatedly, injecting errors at each operation point:
-
First run: error at operation 1
-
Second run: error at operation 2
-
…and so on until all operations succeed
This systematically tests all error handling paths.
Output
Test: happy path
PASSED
Test: partial reads (1 byte at a time)
PASSED
Test: error injection
Runs: 9 (success=2, error=7)
PASSED (all error paths tested)
All tests passed!
Exercises
-
Add a test for EOF handling (what if input doesn’t end with newline?)
-
Test with different max_read_size values
-
Add a test for write errors using
test::write_stream
Next Steps
-
Type-Erased Echo — Compilation firewall pattern