While I begin to wrap up porting reliable.io from C to C++ for my PMO library, I wanted to touch on a quick testing technique I’ve used in Go for quite some time, which I realized I can also use in C++.
Go interface{}, Mock Functions and Tests
In Go we usually define our ‘Abstract classes’ as an interface such as:
type Endpoint interface {
Send(data []byte, len int) error
}
Then, we’d have some concrete implementation that only needs to match the method definition:
type UDPEndpoint struct {
conn *net.UDPConn
}
func (e Endpoint) Send(data []byte, len int) error {
// send data
_, err := conn.WriteToUDP(...)
return err
}
I’m not a huge fan of large complex mock systems, but there is obviously a need to ensure various aspects of a system work, regardless of connections and endpoints and databases.
In these cases I like to build a very basic mock:
type EndpointMock struct {
SendFn func(data []byte, len int) error
}
func (e *EndpointMock) Send(data []byte, len int) error {
return e.SendFn(data, len)
}
Using this, we can simply replace the SendFn member with whatever we want in our test:
func TestEndpoint(t *testing.T) {
mockEndpoint := &EndpointMock{}
mockEndpoint.SendFn = func(data []byte, len int) error {
require.Equal(t, 7, len)
return nil
}
// run test that uses the Endpoint interface to ensure the len is 7 when called!
}
Simple, and very effective for allowing us to customize what type of conditions we want to test, without having to build out a full socket server.
Pretty neat!
Now let’s do this in C++
C++ does not have “interfaces” as in, the keyword. But they are basically abstract classes with pure virtual methods. In my case I want to test the Send function for my client and server. Here’s my abstract SocketSender class:
/**
* @brief Used for testing to allow reliablity tests to pass in custom sender functions
*
*/
class SocketSender
{
public:
/**
* @brief Assumes a client socket, connected and sending to the remote server
*
* @param Buffer The buffer to send
* @param Len Length of buffer
* @return size_t number of bytes sent
*/
virtual size_t Send(const void *Buffer, const int Len) const noexcept = 0;
/**
* @brief Sends data ToAddr
*
* @param ToAddr Address to send the buffer to
* @param Buffer The buffer to send
* @param Len Length of buffer
* @return size_t number of bytes sent
*/
virtual size_t SendTo(struct sockaddr_in &ToAddr, const void *Buffer, const int Len) const noexcept = 0;
};
Now I need to update my Socket class to inherit from SocketSender:
namespace net
{
class Socket : public SocketSender
{
...
}
}
Since this is the only method called by my ReliableEndpoint class, I simply store a reference to the SocketSender &Sock; abstract class, and call Sock.Send(...) as I normally would.
Testing
Inside my C++ test file, I define a new class called MockSock:
struct MockSock : SocketSender {
virtual size_t Send(const void *Buffer, const int Len) const noexcept override
{
if (SendFn)
{
return SendFn(Buffer, Len);
}
return 0;
};
virtual size_t SendTo(struct sockaddr_in &ToAddr, const void *Buffer, const int Len) const noexcept override
{
if (SendFn)
{
return SendFn(Buffer, Len);
}
return 0;
}
std::function<size_t(const void *Buffer, const int Len)> SendFn = nullptr;
};
It inherits from SocketSender and implements the override’s necessary for Send/SendTo. But it also has a single public member std::function<size_t(const void *Buffer, const int Len)> SendFn = nullptr;. This std::function is what I can override in my tests.
In this case I want two endpoints, a sender and receiver. I want the Send function to just directly Call Endpoint.RecievePacket on the opposite endpoint:
MockSock SenderSock{};
MockSock ReceiverSock{};
auto Config = net::ReliableEndpointConfig();
struct sockaddr_in Addr;
auto SenderEndpoint = net::ReliableEndpoint(ValidUser, SenderSock, Addr, Config, 0);
// ...
ReceiverSock.SendFn = [&SenderEndpoint, &Logger, &Crypt](const void *Buffer, const int Len) -> size_t
{
auto Deserialized = Game::Message::GetMessage(Buffer);
REQUIRE(Deserialized->user_id() == 0xd34db33f);
std::vector<unsigned char> Reassembled;
SenderEndpoint.ReceivePacket(Logger, Crypt, *Deserialized, Reassembled);
return Len;
};
auto ReceiverEndpoint = net::ReliableEndpoint(ValidUser, ReceiverSock, Addr, Config, 0);
SenderSock.SendFn = [&ReceiverEndpoint, &Logger, &Crypt](const void *Buffer, const int Len) -> size_t
{
auto Deserialized = Game::Message::GetMessage(Buffer);
REQUIRE(Deserialized->user_id() == 0xd34db33f);
std::vector<unsigned char> Reassembled;
ReceiverEndpoint.ReceivePacket(Logger, Crypt, *Deserialized, Reassembled);
return Len;
};
Using a Lambda, I capture the necessary variables, then make sure that my message is properly deserialized. and forward it to the other endpoints ReceivePacket method. Now all my tests can re-use this idiom and I can test how my system behaves in various situations, such as if packets are dropped.
In my next post I’ll be doing a deep dive into reliable.io and how I modified it to fit with flatbuffers and my design.