2

I need to develop a c++ class that will act as a client for a TCP Server, lets call it myManager, this class will espose a few methods:

  • connect()
  • disconnect()
  • send_command(std::string msg)
  • getStatus()
  • ecc.

All this methods will perform some operations, like for example set some internal variables, call boost::asio::ip::tcp functions to perform the real work, finally the method will check the return value from the boost::asio::ip::tcp calls, update some more internal variables based on the result of the call to boost::asio and complete. How can I mock this function calls in order to perform unit testing in the most efficient way? Writing a mock implementation of the boost.asio library seems a bit overkill.

Notice that:

  • I'm using turtle as a mocking framework but it does not seem to support this functionality, since it only supports mock_objects.
  • I do not want to add an internal object to myManager in order to wrap the calls to boost.asio.
Gionata Benelli
  • 357
  • 1
  • 3
  • 20
  • 2
    The short answer, don't mock directly. Create an abstract baseclass (interface) has methods of what you want to do (e.g. with boost asio). Then create a class that implements the interface and forward to boos asio calls. Then inject that interface into your code and talk to that (dependency injection). Now it is easy to make your own mocks. If you do it well you tests don't even have to link to boost – Pepijn Kramer Sep 21 '21 at 13:31

1 Answers1

0

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.

sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    I think that maybe I have explained really bad my problem. I want to unit test myManager class, which depends on boost.asio for tcp/ip communications. I want to perform Unit Testing without implementing a server to receive the message of myManager class. In cmocka on C I can mock function calls during using -Wl,wrap= in order to test the implementation without really calling the function. Is something like this possible in C++ using boost.test or the Turtle.boost library? Thanks for you answer, it was really educational, from my point of view! – Gionata Benelli Sep 27 '21 at 12:57
  • Everything is possible, but mocking the asio level of things is going to lead to useless tests and a lot of work. I just recommend against it. There is literally no need to do this. Just make your request /appear/ from a (mock) source and forget about asio at the application level. – sehe Sep 27 '21 at 13:05
  • 1
    Maybe I'm wrong in my understanding of mocking. I would simply like to use a framework to automatically generates returns value for the calls to boost.asio function, first time return true, then return false and so on. In order to stimulate all the behaviours of my class interacting to the TCP server without having to write a TCP-server. There is no way to do this? – Gionata Benelli Sep 27 '21 at 13:46
  • No useful way IMO. Nothing wrong with mocking as you describe. Just with mocking individual low-level I/O primitives. All you end up doing is confirming your assumptions about the mock you write yourself, with little tor no relevance to actual operation. – sehe Sep 27 '21 at 14:02
  • In short, mock your higher level application interfaces. I/O is naturally more amenable to integration testing. More that to gain confidence in edge case behavior (unexpected conditions, resource scaling, performance bounds etc) it's natural to have a stub server that exercises all these scenarios. It will be a lot easier to make and maintain and actually prove things about the system under test (instead of proving things about the mock) – sehe Sep 27 '21 at 14:07
  • This answer is unrelated to the question. Where is unit test? – Oguzhan Bolukbas Dec 20 '22 at 04:29
  • @OguzhanBolukbas There's no unit test in the question either. In fact, there was no code. My code merely shows how a mock could be applied. You can then take that and put in whatever Unit test framework you are using. The reason I didn't choose one is because (a) I don't want to assume (b) No online compilers I know support either Boost Test, Catch2, gtest etc – sehe Dec 20 '22 at 15:53