c++socketsasynchronousboostgoogletest

mock AsyncReadStream boost for unit tests


I am faced with a situation where we use http::async_read(...) and http::async_write(...) internally in a class that represents an HTTP session. However, if we want to unit test this class, we would prefer not to open a network connection, with a port and a socket to listen on and correspondingly accept the first call via an acceptor.

Rather, I was hoping to "fake" the behavior of the socket via another AsyncReadStream. I've looked into writing my own, but it seems like a difficult task and search did not reveal much help.

What is an example of an AsyncReadStream that could be useful here to simply stream in an HTTP request and subsequently put the response into, for the purposes of unit testing? If none, s there a template of some sort that I could use to write a barebones one with this very minimal "fake" functionality?


Solution

  • It's almost trivial to model the AsyncReadStream/AsyncWriteStream concepts

    The only complexity is from initiating async operations using CompletionToken patterns:

    E.g.:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <iostream>
    #include <fmt/ranges.h>
    namespace asio = boost::asio;
    using namespace std::literals;
    
    struct MockStream {
        using error_code    = boost::system::error_code;
        using executor_type = asio::any_io_executor;
        MockStream(asio::any_io_executor ex) : ex_(std::move(ex)) {}
        executor_type get_executor() const { return ex_; }
    
        template <typename Token> auto async_write_some(asio::const_buffer buf, Token&& token) {
            return asio::async_initiate<Token, void(error_code, size_t)>( //
                [fallback = ex_](auto h, auto buf) {
                    auto ex = asio::get_associated_executor(h, fallback);
                    asio::dispatch(ex, [=, h = std::move(h)]() mutable {
                        fmt::print("Simulated write of {} completed\n", buf.size());
                        std::move(h)({}, asio::buffer_size(buf));
                    });
                },
                token, buf);
        }
    
        template <typename Token> auto async_read_some(asio::mutable_buffer buf, Token&& token) {
            return asio::async_initiate<Token, void(error_code, size_t)>( //
                [fallback = ex_, ch = mock_data_++](auto h, auto buf) {
                    auto ex = asio::get_associated_executor(h, fallback);
    
    
    
                    std::fill(asio::buffers_begin(buf), asio::buffers_end(buf), ch);
    
                    asio::dispatch(ex, [=, h = std::move(h)]() mutable {
                        if (ch == 'e') {
                            auto partial = (rand() % buf.size()) / 4;
                            fmt::print("Simulated EOF with {} partial read\n", partial);
                            std::move(h)(asio::error::eof, partial);
                        } else {
                            fmt::print("Simulated read of {} completed\n", buf.size());
                            std::move(h)({}, asio::buffer_size(buf));
                        }
                    });
                },
                token, buf);
        }
    
        asio::any_io_executor ex_;
        char                  mock_data_ = 'a';
    };
    
    int main() {
        srand(time(NULL));
        asio::io_context ioc;
    
        MockStream stream(asio::system_executor{});
    
        asio::async_write(stream, asio::buffer("Hello world"sv), [](auto ec, size_t n) {
            fmt::print("Write completed with {} and {} bytes written\n", ec.message(), n);
        });
    
        std::array<char, 10> buf;
        asio::async_read(stream, asio::buffer(buf), [&buf](auto ec, size_t n) {
            fmt::print("Read completed with {} and {} bytes read: {}\n", ec.message(), n, buf);
        });
    
        asio::streambuf sbuf;
        asio::async_read_until(stream, sbuf, "d", [&sbuf](auto ec, size_t n) {
            fmt::print("Read-until completed with {} and {} bytes read\n", ec.message(), n);
            // std::cout << "Streambuf: " << &sbuf << std::endl;
            sbuf.consume(sbuf.size());
        });
    
        asio::async_read_until(stream, sbuf, "z", [&sbuf](auto ec, size_t n) {
            fmt::print("Read-until completed with {} and {} bytes read\n", ec.message(), n);
            std::cout << "Streambuf: " << &sbuf << std::endl;
        });
    
        ioc.run();
    }
    

    Printing e.g.

    enter image description here

    LINKS

    Similar implementations here: