Testing
Code that consumes HTTP responses is difficult to test against a live server.
Burl provides two seams for testing without a network: a factory that
synthesizes a response directly, and a configuration hook that replaces
how the client establishes connections.
Synthesizing a Response
test::response_factory builds a genuine response from a status,
headers, and a body, with no client or server involved — suitable for
unit-testing body conversions, status handling, and anything else that takes a
response:
auto r = burl::test::response_factory(http::status::ok)
.header(http::field::content_type, "application/json")
.body({ R"({"key":"value"})" })
.create();
auto v = co_await r.as<json::value>();
// assert v.at("key") == "value"
The body is given as a vector of strings, and each element is delivered as a separate read. This is what makes the factory useful for more than a happy path: the split controls how the body is fragmented on the wire, so a conversion can be tested against a body that arrives in several reads.
Injecting Read Failures
response_factory::create accepts a
capy::test::fuse. Under an armed fuse it injects a read failure at successive
points in the body, so a single test can walk a conversion through a failure at
each read and confirm it surfaces the error rather than mis-parsing a partial
body:
capy::test::fuse f;
auto factory = burl::test::response_factory(http::status::ok)
.header(http::field::content_type, "application/json")
.body({ R"({"user")", R"(:"John")", R"(})" });
auto outcome = f.armed([&](capy::test::fuse&) -> capy::task<>
{
auto r = factory.create(f);
auto [ec, doc] = co_await r.try_as<nlohmann::json>();
if(ec)
co_return; // a read failed; the conversion surfaced it
// assert(doc.at("user") == "John");
});
// assert(outcome.success);
Replacing Connection Establishment
For an end-to-end test through a real client,
config::connect_handler replaces the client’s name
resolution, connection, proxy negotiation, and TLS handshake. Whenever the pool
needs a new connection it calls your handler, which returns a capy::any_stream
for the client to speak HTTP over:
burl::client::config cfg;
cfg.connect_handler =
[](urls::url_view) -> capy::io_task<capy::any_stream>
{
auto [a, b] = capy::test::make_stream_pair();
// drive b from the test as the "server"; hand a to the client
co_return { {}, capy::any_stream(std::move(a)) };
};
burl::client client(co_await capy::this_coro::executor, tls_ctx, cfg);
The handler receives the target URL and yields the stream. Pairing it with
capy::test::make_stream_pair gives one end to the client and keeps the other
in the test to play the server, letting the full request path run in process.
The hook is also a way to connect over an already-open tunnel or a Unix domain
socket in production code.
Next Steps
-
Responses — The body functions that a synthesized response feeds
-
Error Handling — The failures a fuse helps you cover
-
Reference —
test::response_factoryin detail