1

我需要开发一个作为 TCP 服务器客户端的 c++ 类,我们称之为 myManager,这个类将包含一些方法:

  • 连接()
  • 断开()
  • send_command(std::string msg)
  • 获取状态()
  • 电子抄送

所有这些方法都会执行一些操作,例如设置一些内部变量,调用 boost::asio::ip::tcp 函数来执行实际工作,最后该方法将检查 boost::asio:: 的返回值ip::tcp 调用,根据 boost::asio 调用的结果更新一些内部变量并完成。如何模拟此函数调用以便以最有效的方式执行单元测试?编写 boost.asio 库的模拟实现似乎有点矫枉过正。

请注意:

  • 我使用turtle 作为模拟框架,但它似乎不支持这个功能,因为它只支持mock_objects。
  • 我不想将内部对象添加到 myManager 以包装对 boost.asio 的调用。
4

1 回答 1

1

您正在描述一组完全合理的模拟函数。“mock整个Asio库”的障碍到底出现在你的实现思路的什么地方?

让我们以这个答案为例:它使用 Boost Asio 与两个 Stockfish 国际象棋引擎进程进行异步接口。它还使用协程来实现该类的接口非常小,因此我们可以像这样制作一个 Mock 引擎:

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;
};

如您所见,它只是玩着名的做空股票游戏。您可以针对模拟引擎构建和运行游戏,甚至无需链接 Boost Context 或 Coroutine,或包含任何 Boost 标头。

这是一个独立的程序,显示了它的全部作用:

活在魔杖盒上

  • 文件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;
     };
    
  • 文件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
     };
    
  • 文件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>();
     }
    

打印股票游戏,然后是您的 sotckfish 引擎当时的灵感:

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

概括

如您所见,您甚至可以在不考虑 Asio 实现的情况下进行模拟。当然,你的 mock更有状态,所以看起来会比这更智能,但原理是一样的。

于 2021-09-21T15:17:33.147 回答