You're describing a perfectly sane set of functions to mock. Where exactly does the obstacle of "mocking the entire Asio library" appear in your implementation ideas?
Let's take this answer for exaple: it uses Boost Asio to asynchronously interface with two Stockfish chess engine processes. It also uses coroutines to achieve that The interface of the class is pretty minimal, so we can make a Mock engine like so:
struct MockEngine {
/*
*Alexander Alekhine - Vasic C15
*Simul, 35b, Banja Luka YUG
*
*1. e4 e6 2. d4 d5 3. Nc3 Bb4 4. Bd3 Bxc3+ 5. bxc3 h6 6. Ba3 Nd7 7. Qe2
*dxe4 8. Bxe4 Ngf6 9. Bd3 b6 10. Qxe6+ fxe6 11. Bg6# 1-0
*/
static constexpr std::array s_stock_game{
"e2e4", "e7e6", "d2d4", "d7d5", "b1c3", "f8b4", "f1d3",
"b4dc3", "b2c3", "h7h6", "c1a3", "b8d7", "d1e2", "d5e4",
"d3e4", "g8f6", "e4d3", "b7b6", "e2e6", "f7e6", "d4g6",
};
MockEngine(MoveList& game) : _game(game) {}
std::string make_move()
{
if (_game.size() < s_stock_game.size())
return s_stock_game[_game.size()];
return "(none)";
}
private:
MoveList& _game;
};
As you can see it just plays a famous short stock game. You can build and run a game against the mock engine without even linking Boost Context or Coroutine, or including any of the Boost headers.
Here's a self-contained program that shows it all in action:
Live On Wandbox
File mock_engine.h
#include <iomanip>
#include <array>
#include <string>
#include <deque>
using MoveList = std::deque<std::string>;
struct MockEngine {
/*
*Alexander Alekhine - Vasic C15
*Simul, 35b, Banja Luka YUG
*
*1. e4 e6 2. d4 d5 3. Nc3 Bb4 4. Bd3 Bxc3+ 5. bxc3 h6 6. Ba3 Nd7 7. Qe2
*dxe4 8. Bxe4 Ngf6 9. Bd3 b6 10. Qxe6+ fxe6 11. Bg6# 1-0
*/
static constexpr std::array s_stock_game{
"e2e4", "e7e6", "d2d4", "d7d5", "b1c3", "f8b4", "f1d3",
"b4dc3", "b2c3", "h7h6", "c1a3", "b8d7", "d1e2", "d5e4",
"d3e4", "g8f6", "e4d3", "b7b6", "e2e6", "f7e6", "d4g6",
};
MockEngine(MoveList& game) : _game(game) {}
std::string make_move()
{
if (_game.size() < s_stock_game.size())
return s_stock_game[_game.size()];
return "(none)";
}
private:
MoveList& _game;
};
File uci_engine.h
#include <iostream>
static inline std::ostream debug_out(nullptr /*std::cerr.rdbuf()*/);
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/process.hpp>
#include <boost/process/async.hpp>
#include <boost/spirit/include/qi.hpp>
namespace bp = boost::process;
namespace qi = boost::spirit::qi;
using boost::asio::yield_context;
using namespace std::literals;
struct UciEngine {
UciEngine(MoveList& game) : _game(game) { init(); }
std::string make_move()
{
std::string best, ponder;
boost::asio::spawn([this, &best, &ponder](yield_context yield) {
auto bestmove = [&](std::string_view line) { //
return qi::parse( //
line.begin(), line.end(),
"bestmove " >> +qi::graph >> -(" ponder " >> +qi::graph) >>
qi::eoi,
best, ponder);
};
bool ok = send(_game, yield) //
&& command("go", bestmove, yield);
if (!ok)
throw std::runtime_error("Engine communication failed");
});
run_io();
return best;
}
private:
void init()
{
boost::asio::spawn([this](yield_context yield) {
bool ok = true //
&& expect([](std::string_view banner) { return true; }, yield) //
&& command("uci", "uciok", yield) //
&& send("ucinewgame", yield) &&
command("isready", "readyok", yield);
if (!ok)
throw std::runtime_error("Cannot initialize UCI");
});
run_io();
}
bool command(std::string_view command, auto response, yield_context yield)
{
return send(command, yield) && expect(response, yield);
}
bool send(std::string_view command, yield_context yield)
{
debug_out << "Send: " << std::quoted(command) << std::endl;
using boost::asio::buffer;
return async_write(_sink, std::vector{buffer(command), buffer("\n", 1)},
yield);
}
bool send(MoveList const& moves, yield_context yield)
{
debug_out << "Send position (" << moves.size() << " moves)"
<< std::endl;
using boost::asio::buffer;
std::vector bufs{buffer("position startpos"sv)};
if (!moves.empty()) {
bufs.push_back(buffer(" moves"sv));
for (auto const& mv : moves) {
bufs.push_back(buffer(" ", 1));
bufs.push_back(buffer(mv));
}
}
bufs.push_back(buffer("\n", 1));
return async_write(_sink, bufs, yield);
}
bool expect(std::function<bool(std::string_view)> predicate,
yield_context yield)
{
auto buf = boost::asio::dynamic_buffer(_input);
while (auto n = async_read_until(_source, buf, "\n", yield)) {
std::string_view line(_input.data(), n > 0 ? n - 1 : n);
debug_out << "Echo: " << std::quoted(line) << std::endl;
bool matched = predicate(line);
buf.consume(n);
if (matched) {
debug_out << "Ack" << std::endl;
return true;
}
}
return false;
}
bool expect(std::string_view message, yield_context yield)
{
return expect([=](std::string_view line) { return line == message; },
yield);
}
void run_io()
{
_io.run();
_io.reset();
}
boost::asio::io_context _io{1};
bp::async_pipe _sink{_io}, _source{_io};
bp::child _engine{"stockfish", bp::std_in<_sink, bp::std_out> _source, _io};
MoveList& _game;
std::string _input; // read-ahead buffer
};
File test.cpp
#include "mock_engine.h"
#include "uci_engine.h"
template <typename Engine>
void run_test_game() {
MoveList game;
Engine white(game), black(game);
for (int number = 1;; ++number) {
game.push_back(white.make_move());
std::cout << number << ". " << game.back();
game.push_back(black.make_move());
std::cout << ", " << game.back() << std::endl;
if ("(none)" == game.back())
break;
}
}
int main() {
run_test_game<MockEngine>();
run_test_game<UciEngine>();
}
Which prints the stock game, followed by whatever inspiration your sotckfish engine has at the time:
1. e2e4, e7e6
2. d2d4, d7d5
3. b1c3, f8b4
4. f1d3, b4dc3
5. b2c3, h7h6
6. c1a3, b8d7
7. d1e2, d5e4
8. d3e4, g8f6
9. e4d3, b7b6
10. e2e6, f7e6
11. d4g6, (none)
1. d2d4, d7d5
2. g1f3, g8f6
3. e2e3, c7c5
4. b1c3, e7e6
5. f1e2, f8e7
6. e1g1, b8c6
7. d4c5, e7c5
8. b2b3, e8g8
9. c3a4, c5d6
10. c1b2, e6e5
11. c2c4, d5c4
... etc long boring computer games
Summary
As you can see you can mock without even thinking of the Asio implementation. Of course, your mock will be more stateful, so will look more intelligent than this, but the principle remains the same.