Jak praktycznie testować operacje sieciowe w boost.asio?

0

Czołem,
jak mam testować takie operacje sieciowe np. czy klient rzeczywiście się łączy, odbiera/wysyła prawidłowe ilości danych itp... Jakieś mocki? Używaliście może test::stream, co sądzicie?

czy startowanie jakiegoś serwera testowego ma sens?

Macie jakieś rady gdzie zacząć albo jak wy to robicie?

2

Testy się pisze by sprawdzać własny kod.
Ergo piszesz swój kod tak, by boost::asio było twoją zależnością, którą wstrzykujesz do swojego kodu, a w testach wstrzykujesz wersję mock.

Ten test::stream trzeba by było wypróbować w boju, by stwierdzić czy to ma ręce i nogi.

0

No tak, ale jak niby mam zrobić atrapę takiego socketa i na nim robić wszystkie operacje? O to mi chodzi - nie wiem z czym to się je.

edit:
czyli co, robić serwer testowy i na nim próbować popsuć swój kod?

2

To jest złożony problem. Rozsądnie jest założyć, że asio czy jakiego tam innego frameworka używasz działa dobrze i tak jak @MarekR22 napisał, wstrzykujesz go jako zależność, a do testów mockujesz interfejs i sprawdzasz tylko czy funkcja send została wywołana ew., że receive zwróci jakiś tam bufor.
Przykład będzie nie asio, tylko QT, bo akurat taki mam pod ręką. Potrzebowałem czytać czujnik ćisnienia na UARTcie, ale chciałem być w stanie to testować bez ciągłego pompowania, no to:

      MockedIoDevice mock{{0x01, 0x02, 0x0f, 0xf0, 0xf1, 0x1f}}; // testuję dla takich danych wejściowcyh
      mock.open(QIODevice::ReadOnly | QIODevice::Unbuffered);
      IODeviceReader serialPortReader{mock};

Tenże mock implementuje interfejs pozwalający readerowi czytać go jak uart:

class MockedIoDevice : public QIODevice
{
    Q_OBJECT
public:
    MockedIoDevice(std::initializer_list<unsigned char> bytes, QObject* parent = nullptr);
    qint64 bytesAvailable() const override;
    qint64 size() const override;
    bool canReadLine() const override;
    void close() override { }
    bool isSequential() const override;
    bool atEnd() const override;
    qint64 pos() const override;
    bool seek(qint64 pos) override;

protected:
    qint64 readData(char* data, qint64 maxSize) override;
    qint64 writeData(const char* data, qint64 maxSize) override;
    qint64 readLineData(char* data, qint64 maxSize) override;

private:
    std::vector<unsigned char> m_readoutBuf; 
    decltype(m_readoutBuf)::const_iterator m_currentReadPos = m_readoutBuf.cbegin();
};

W Boost::Asio nie wiem czy będziesz miał analogiczny interfejs, ale podejrzewam, że będzie w stanie coś opędzić templatkami?

EDIT: no a jak nie możesz mockować to
a. robisz testy end-to-end. Ale to niekoniecznie dobrze.
b. Jeżeli jesteś na jakiejś platforme POSIXowej to możesz użyć LD_PRELOAD czy tam --wrap przekazanego do linkera i podmienić syscalle, ktorych ASIO używa.
https://stackoverflow.com/questions/46444052/how-to-wrap-functions-with-the-wrap-option-correctly

0

Temat jest bardzo szeroki.
Jak chcesz konkretnej pomocy, musisz skonkretyzować pytanie.

0

no ciężary mega napisać te testy, nie sądziłem, że będzie z tym tyle problemów.

Muszę przegrzebać gh i znaleźć jakieś projekty gdzie ludzie właśnie testują swoje funkcje wykorzystujące asio, bo muszę mieć jakiś ogólny zarys jak to ma wyglądać. Koniec końców dobiorę się do tego test streama, którego podesłałem i z nim będę się bawić, bo już dość czasu zmarnowałem na napisanie samego mocka serwera (który i tak nie działa XDD)

0

Nie wiem na ile asio jest w headerach a na ile w libce. Jak jest raczej w libce a headery to czyste interfejsy (ale z boostem to nie jest oczywiste) to możesz spróbować mockować na etapie linkowania...

0

Dla REST'a używam programu PostMan: https://www.postman.com/product/rest-client/
On potrafi zapisać sporą ilość ręcznie wpisanych zapytań oraz odpowiedzi na nie.
Po czym można go przestawić w tryb symulacji, i jak znajdzie odpowiednie zapytanie w zapamiętanych zapytaniach to odpowiada zapamiętaną odpowiedzią. jedynie się łączysz z localhost ale to można przestawić (pod windows.: c:\Windows\System32\drivers\etc\hosts)
Jeżeli to nie jest REST to co za problem napisać coś podobnego?
Zauważ że nie potrzebujesz tak rozbudowanych funkcjonalności jak PostMan

2
Cyberah napisał(a):

no ciężary mega napisać te testy, nie sądziłem, że będzie z tym tyle problemów.

Muszę przegrzebać gh i znaleźć jakieś projekty gdzie ludzie właśnie testują swoje funkcje wykorzystujące asio, bo muszę mieć jakiś ogólny zarys jak to ma wyglądać. Koniec końców dobiorę się do tego test streama, którego podesłałem i z nim będę się bawić, bo już dość czasu zmarnowałem na napisanie samego mocka serwera (który i tak nie działa XDD)

  1. Daj jakiś przykład swojego kodu do przetestowania to dostaniesz przykład
  2. Pisanie testów do gotowego kodu jest trudne i nieprzyjemne. Pisanie kodu do gotowych testów jesz szybkie i przyjemne.
0

@MarekR22: no dobra, to rzuć proszę okiem:
metoda, którą chcę przetestować wygląda mniej więcej tak:

void Client::connect(asio::ip::address const& ip_address, unsigned short const  port) {
    asio::ip::tcp::endpoint ep{ ip_address, port };
    asio::ip::tcp::socket sock{ m_ioc }; //asio::io_context;
    sock.async_connect(ep,
        [this](auto const& ec) {
            on_connected(ec);
    });
}

on_connected() jest dość prosty:

void Client::on_connected(system::error_code const& ec) {
    if (!ec)
        emit connected();
    else
        emit badConnect(ec);
}

i ja to dotychczas próbuje testować tak, że chcę złapać sygnał używając QSignalSpy:

TEST(ClientTest, passingGoodArgumentsToClientConnectMethodProperlyConnects)
{
    asio::ip::address const ip_address{asio::ip::address::from_string("127.0.0.1")};
    auto const port{3333};

    Client client;
    QSignalSpy connectedEmitted{&client, SIGNAL(connected)};

    client.connect(ip_address, port); //boom! jak connectuje to crashuje mi test
    //EXPECT_EQ(connectedEmitted.count(), 1);

    //client.disconnect();
}

Serwer ofc w międzyczasie mam włączony i nasłuchuje na porcie 3333, ale i tak crashuje.
Nie mam już pomysłów :(

2

Coś mi się wydaje że pomyliłeś pisanie testów z debugowaniem.
Celem testów jest odpowiedz na jedno z dwóch pytań:

  • Czy już zaczęło działać poprawnie [T/N]?
  • Czy wciąż działa poprawnie [T/N]?

Natomiast skoro crashuje to już masz odpowiedź na to pytanie, szukać należy za pomocą debugiera.

0

Dobra, po przerwie ogarnąłem to, ale rozwiązanie ciągnie za sobą dwie wady: serwer musi być dostępny(na razie nie napisałem jeszcze mocka) i przede wszystkim muszę usypiać wątek na parę milisekund. Co myślicie? Nie wiem czy usypianie testów to dobry pomysł, bo jak ich się uzbiera to będą się długo wątki lenić.

0

Myślimy, że to jest co najmniej code smell. Tzn. ja myślę za siebie ;) ale mam wrazenie, że pozostali Panowie się zgodzą.
Czemu w sumie musi coś spać?

0

bo klient asynchronicznie się łączy z serwerem i jak nie uśpię go na ~5 milisekund, to zwyczajnie nie zdąży wywołać handlera po połączeniu (który wysyła sygnały) :

    client->connect(ip_address, port);
    std::this_thread::sleep_for(std::chrono::milliseconds(5));

    EXPECT_EQ(connectedEmitted.count(), 1);
    EXPECT_EQ(badConnectEmitted.count(), 0);

connect od razu zwraca, bo to praktycznie tylko asio::async_connect().

generalnie pewnie da się to lepiej zrobić ze zmiennymi warunkowymi, ale to jeszcze muszę rozkminić.

1

Ale QSignalSpy przeca ma wait? https://doc.qt.io/qt-5/qsignalspy.html#wait

3
MarekR22 && Cyberah skomentował(a):
  • Nie gryzie ci się kontekst boost:asio z QEventLoop Qt? — MarekR22
  • ani trochę:P — Cyberah
  • w zasadzie to jestem miło zaskoczony, że to działa, bo rzeczywiście parę osób odradzało mieszanie boost, wątków i Qt5, bo może być buba, ale na razie jest git, a sygnały wysyłane z handlerów mogą być elegancko przetworzone dalej. — Cyberah

Żeby było jasnet zarówno QEventLoop jak boost::asion::io_context robią to samo.

  • QEventLoop wywoławane jest przez QApplication:exec() i blokuje wątek, aż do nadejścia jakiegoś zdarzenia od systemu.
  • tak samo boost::asion::io_context::run - blokuje wątek aż do nadejścia zdarzeń od systemu.

Ergo oba nie mogą działać na jednym wątku, bo jeden be∂zie blokować drugi.
Jeśli boost::asion::io_context::run robisz na osobnym wątku, to jest szansa, że to działa. Niestety z moich obserwacji wynika, że większość developerów z medium level nie ogarnia wielowątkowości, a beginning wydaje się tylko, że umieją wielowątkowość.

Co do samego testu. To może być coś w tym stylu (pisane z palca).

class IAsioSocket {
public:
    virtual ~IAsioSocket() {}

    virtual void async_connect(const endpoint_type & peer_endpoint, ConnectHandler && handler) = 0;
};

class INetworkDependencies {
public:
    virtual ~INetworkDependencies() {}

    virtual std::unique_ptr<IAsioSocket> makeSocket() = 0;
};

class Client {
public:
    explicit Client(INetworkDependencies *dependencies)
        : m_dependencies{dependencies}
        , m_socket{m_dependencies->makeSocket()}
    {}

    void connect(asio::ip::address const& ip_address, unsigned short const  port) {
        asio::ip::tcp::endpoint ep{ ip_address, port };
        m_socket->async_connect(ep,
            [this](auto const& ec) {
                on_connected(ec);
        });
    }

    void on_connected(system::error_code const& ec) {
        if (!ec)
            emit connected();
        else
            emit badConnect(ec);
    }

signal:
    void connected();
    void badConnect(system::error_code const& ec);

private:
    INetworkDependencies *m_dependencies;
    std::unique_ptr<IAsioSocket> m_socket;
};
class MockAsioSocket : public IAsioSocket {
public:
    MOCK_METHOD(void, async_connect, (const endpoint_type & peer_endpoint, ConnectHandler && handler), (override));
};

class MockNetworkDependencies : public INetworkDependencies {
public:
    MOCK_METHOD(std::unique_ptr<IAsioSocket>, makeSocket, (), (override));
};
class ClientTest : public testing::Test
{
public:
     void SetUp() override
     {
         constructClient();
     }

     void constructClient()
     {
           EXPECT_CALL(mockAsio, makeSocket()).WillOnce(Invoke([this] {
                 auto socket = std::make_unique<MockAsioSocket>();
                 mockSocket = socket.get();
                 return socket;
           }));

           client = std::make_unique<Client>(&mockAsio);
           ASSERT_TRUE(Mock::VerifyAndClearExpectations(&mockAsio));

          errorSpy = std::make_unique<QSignalSpy>(client, SIGNAL(badConnect(const system::error_code&)));
          successSpy = std::make_unique<QSignalSpy>(client, SIGNAL(connected()));
     }

     void checkNoSignals()
     {
         ASSERT_EQ(errorSpy.count(), 0);
         ASSERT_EQ(successSpy.count(), 0);
     }

     void startConnecting()
     {
           EXPECT_CALL(*mockSocket, async_connect(_, _))
                 .WillOnce(SaveArg<1>(&connectHandler));
           client->connect(TestAddress, TestPort);
           ASSERT_TRUE(Mock::VerifyAndClearExpectations(mockSocket));
     }

     void successfullConnect()
     {
         ASSERT_TRUE(!!connectHandler);
         ASSERT_EQ(errorSpy.count(), 0);
         ASSERT_EQ(successSpy.count(), 0);

         connectHandler(TestSuccessValue);

         ASSERT_EQ(errorSpy.count(), 0);
         ASSERT_EQ(successSpy.count(), 1);
     }

     MockNetworkDependencies mockAsio;
     MockAsioSocket *mockSocket = nullptr;

     std::unique_ptr<QSignalSpy> errorSpy;
     std::unique_ptr<QSignalSpy> successSpy;

     std::unique_ptr<Client> client;
     ConnectHandler connectHandler;
};

class ClientConstructedTest : public TestClientTest
{};

TEST_F(ClientConstructedTest, connecInvokesBoostAsioAsyncConnect)
{
     ASSERT_NO_FATAL_FAILURE(startConnecting());
}

class ConnectingClientTest : public TestClientTest
{
    void SetUp() overide
    {
          TestClientTest::SetUp();
          ASSERT_NO_FATAL_FAILURE(startConnecting());
    }
};


TEST_F(ConnectingClientTest, whenAsioReportsErrorClientReportsError)
{
     ASSERT_NO_FATAL_FAILURE(checkNoSignals());
     connectHandler(TestErrorValue);

     ASSERT_EQ(errorSpy.count(), 1);
     ASSERT_EQ(successSpy.count(), 0);
}


TEST_F(ConnectingClientTest, whenAsioReportsSuccessClientEntersConnectedState)
{
     ASSERT_NO_FATAL_FAILURE(checkNoSignals());
     ASSERT_NO_FATAL_FAILURE(successfullConnect());
}

Swoją drogą, to że asio::ip::tcp::socket sock{ m_ioc } masz jako zmienną lokalną, kiedy używasz asynchronicznego API to dość poważny błąd.
W moim kodzie jest to poprawione.

1 użytkowników online, w tym zalogowanych: 0, gości: 1