std::function, Lambda and Testing

Published by

on

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.