/premock

Mock C functions using the preprocessor

Primary LanguageC++Boost Software License 1.0BSL-1.0

premock

| Build Status | Build Status |

This header-only (C++) / single-module (D) library makes it possible to replace implementations of C functions (and C++ free functions) with C++ or D callables (functions, lambdas, function objects) for unit testing.

The motivation for premock is to write new tests for legacy C codebases that were not written with testability in mind. The example with send below is indicative of that. I would expect newly written code to not have a hard-coded IO call in the middle of its implementation.

It works by using the preprocessor to redefine the functions to be mocked in the files to be tested by prepending ut_premock_ to them instead of calling the "real" implementation. This ut_premock_ function then forwards to a std::function / delegate of the appropriate type that can be changed at runtime to a C++/D callable, respectively.

An example of mocking the BSD socket API send function would be to have a header like this:

#ifndef MOCK_NETWORK_H
#define MOCK_NETWORK_H
#    define send ut_premock_send
#endif

The build system would then insert this header before any other #includes via an option to the compiler (-include for gcc/clang). This could also be done with -D but in the case of multiple functions it's easier to have all the redefinitions in one header.

Now all calls to send are actually to ut_premock_send. This will fail to link since ut_premock_send doesn't exist. To implement it in C++ (see below for D), the test binary should be linked with an object file from compiling this code (e.g. called mock_network.cpp):

#include "mock_network.hpp"
extern "C" IMPL_MOCK(4, send); // the 4 is the number of parameters "send" takes

This will only compile if a header called mock_network.hpp exists with the following contents:

#include "premock.hpp"
#include <sys/socket.h> // to have access to send, the original function
DECL_MOCK(send); // the declaration for the implementation in the cpp file

Now test code can do this:

#include "mock_network.hpp"

TEST(send, replace) {
    REPLACE(send, [](auto...) { return 7; });
    // any function that calls send from here until the end of scope
    // will call our lambda instead and always return 7
}

TEST(send, mock) {
    auto m = MOCK(send);
    m.returnValue(42);
    // any function that calls send from here until the end of scope
    // will get a return value of 42
    function_that_calls_send();
    // check last call to send only
    m.expectCalled().withValues(3, nullptr, 0, 0); //checks values of last call
    // check last 3 calls:
    m.expectCalled(3).withValues({make_tuple(3, nullptr, 0, 0),
                                  make_tuple(5, nullptr, 0, 0),
                                  make_tuple(7, nullptr, 0, 0)});
}

TEST(send, for_reals) {
    //no MOCK or REPLACE, calling a function that calls send
    //will call the real McCoy
    function_that_calls_send(); //this will probably send packets
}

If neither REPLACE nor MOCK are used, the original implementation will be used.

Please consult the example test file or the unit tests for more.

In D, it's simpler, one module (e.g. mock_network.d) would have to do this:

import premock;
mixin ImplCMock!("send", long, int, const(void)*, size_t, int);

The D version has the function signature. That's because for the function to be usable in D it'd have to be declared anyway whereas C++ can just #include the relevant header.

The D equivalents of MOCK and REPLACE macros are the mock and replace template mixins:

unittest {
    mixin replace!("send", (a, b, c, d) => 7);
    // any function calling send from here until the end of scope
    // will call our lambda instead and always return 7
}

unittest {
    mixin mock!"send"; // creates a local variable called "m"
    m.returnValue(42);
    function_that_calls_send();
    // check last call to send only
    m.expectCalled.withValues(3, cast(const(void*))null, 0UL, 0);
    // check last 3 calls
    import std.typecons; // for tuple
    m.expectCalled().withValues(tuple(...), tuple(...), tuple(...));
}

Please consult the example test file and the unit tests in implementation for more.