【问题标题】:How to Mock calls to boost.asio library?如何模拟对 boost.asio 库的调用?
【发布时间】:2021-11-15 01:30:27
【问题描述】:

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

  • 连接()
  • 断开连接()
  • send_command(std::string msg)
  • getStatus()
  • ecc.

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

注意:

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

【问题讨论】:

  • 简短的回答,不要直接模拟。创建一个抽象基类(接口)具有您想要做的方法(例如使用 boost asio)。然后创建一个实现该接口的类并转发给 boos asio 调用。然后将该接口注入您的代码并与之对话(依赖注入)。现在很容易制作自己的模拟。如果你做得好,你的测试甚至不必链接到 boost

标签: c++ boost mocking turtle-mock


【解决方案1】:

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

让我们看一下answer for exaple:它使用 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 标头。

这是一个独立的程序,展示了所有的实际操作:

Live On Wandbox

  • 文件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 实现的情况下进行模拟。当然,您的模拟更有状态,因此看起来会比这更智能,但原理保持不变。

【讨论】:

  • 我想也许我已经解释了我的问题。我想对 myManager 类进行单元测试,该类依赖于 boost.asio 进行 tcp/ip 通信。我想在不实现服务器来接收 myManager 类的消息的情况下执行单元测试。在 C 上的 cmocka 中,我可以在使用 -Wl,wrap= 期间模拟函数调用,以便在不真正调用函数的情况下测试实现。使用 boost.test 或 Turtle.boost 库的 C++ 中是否有可能发生这样的事情?谢谢你的回答,从我的角度来看,这真的很有教育意义!
  • 一切皆有可能,但嘲笑事物的asio级别会导致无用的测试和大量的工作。我只是建议反对它。实际上没有必要这样做。只需从(模拟)源发出您的请求 /appear/ ,然后在应用程序级别忘记 asio。
  • 也许我对嘲笑的理解是错误的。我只是想使用一个框架为调用 boost.asio 函数自动生成返回值,第一次返回 true,然后返回 false 等等。为了激发我的班级与 TCP 服务器交互的所有行为,而无需编写 TCP 服务器。有没有办法做到这一点?
  • IMO 没有用处。正如你所描述的那样嘲笑没有错。只需模拟单个低级 I/O 原语。您最终所做的只是确认您对自己编写的模拟的假设,与实际操作几乎没有相关性。
  • 简而言之,模拟您的更高级别的应用程序接口。 I/O 自然更适合集成测试。更重要的是,为了获得对边缘情况行为(意外条件、资源扩展、性能限制等)的信心,拥有一个可以练习所有这些场景的存根服务器是很自然的。制作、维护和实际证明关于被测系统的事情会容易得多(而不是证明关于模拟的事情)
猜你喜欢
  • 1970-01-01
  • 2012-07-26
  • 1970-01-01
  • 2019-12-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多