Mocking Functions 🔗

In non-trivial programs, certain scenarios can be challenging or even impossible to reproduce during testing. Examples include out-of-memory errors, code that involves random number generators, and code that interacts with external resources, such as databases or network requests. To effectively test these scenarios, the Audition testing framework provides built-in support for function mocking.

Function mocking lets you simulate the behavior of a function. This enables you to isolate your tests from external dependencies and focus on the functionality you want to validate. In Audition, there are two built-in ways to mock functions: fakes and stubs.

Mocking with Fakes 🔗

Fakes let you intercept a function call and redirect it to a different function known as the fake function. For the fake function to work correctly, its type signature must be identical to the original function’s type signature, otherwise, the behavior is undefined.

The fake function can include custom logic for parameter validation, call through to the original function, and return its own test-specific return value.

In Audition, C functions are faked with the FAKE macro. This macro accepts two arguments: the function to fake and the function to redirect to.

In the following example, the FAKE macro is used to redirect all calls from the foo function to the fake_foo function.

int foo(void); // forward declaration

static int fake_foo(void) {
    return 42;
}

TEST(spam, eggs) {
    FAKE(foo, fake_foo);             // mocks foo()
    printf("foo: %d\n", fake_foo()); // prints 42
}

Mocking with Stubs 🔗

Stubs let you intercept a function call and return a hard-coded value without needing to write a dedicated fake function. This is useful in situations where the primary purpose of your fake function is to simply return a predetermined value.

In Audition, C functions are stubbed with the STUB macro. This macro accepts two arguments: the function to stub and a constant to return.

The following example is identical to the fake function example except there is no need to implement a separate fake_foo function.

TEST(spam, eggs) {
    STUB(foo, 42);              // mocks foo()
    printf("foo: %d\n", foo()); // prints 42
}

Stubbing a function requires the type of the constant to match the return type of the function being stubbed, e.g. if a function returns an integer, do not stub it to return a floating-point value. If it returns a long integer, then add the L suffix. If the return types mismatch, then the behavior is undefined.

Mocking Lifecycle 🔗

Understanding the lifecycle of mocks is crucial for effective testing. Here’s what you need to know about the duration, management, and cleanup of mocks.

Validity of Mocks 🔗

Mocks created during a test are valid only for the duration of that test case. Once the test case completes, regardless of whether it passes or fails, all mocks are automatically unregistered. This automatic cleanup helps maintain test isolation and prevents interference between tests.

If you need multiple test cases to use the same mock, then you should mock the function in the TEST_SETUP fixture.

TEST_SETUP(spam) {
    STUB(foo, 42); // mocks foo() before each test case
}

TEST(spam, eggs) {
    printf("foo: %d\n", foo()); // prints 42
}

TEST(spam, ham) {
    printf("foo: %d\n", foo()); // prints 42
}

Unmocking 🔗

In Audition, you can remove the mock from a function using the RESTORE macro. This is useful in situations where you need to revert to the original functions behavior partway through a test case. The following example unmocks the foo function right after stubbing it, allowing the original foo to be invoked.

TEST(spam, eggs) {
    STUB(foo, 42);              // mock foo()
    printf("foo: %d\n", foo()); // prints 42
    RESTORE(foo);               // unregister the mock for foo()
    printf("foo: %d\n", foo()); // calls original foo()
}

Suppressing All Active Mocks 🔗

In some situations, you may need to temporarily suppress all active mocks. For example, if you want to call your own test utility function it would be smart to suppress all active mocks to ensure your utility function doesn’t accidentally invoke a mocked function.

To suppress all active mocks, Audition provides the SUSPEND_MOCKS and RESTORE_MOCKS pair of macros to suspend and restore all active mocks. When SUSPEND_MOCKS is called, all active mocks are suspended until RESTORE_MOCKS is called. In the following example the foo function is mocked, then suspended allowing the original foo implementation to be called, then the mock is restored again.

TEST(spam, eggs) {
    STUB(foo, 42);              // mock foo()
    printf("foo: %d\n", foo()); // prints 42

    SUSPEND_MOCKS();            // suspends all mocks
    printf("foo: %d\n", foo()); // calls original foo()
    RESTORE_MOCKS();            // restores all mocks

    printf("foo: %d\n", foo()); // prints 42
}

The SUSPEND_MOCKS macro can be called multiple times as long as each call is paired with a call to RESTORE_MOCKS. In this sense, the macros have a “stacking behavior” where SUSPEND_MOCKS acts like a “push” operation to suspend mocks, and RESTORE_MOCKS acts like a “pop” operation to re-enable mocks, but only if the “stack” is empty. If a test completes without enough calls to RESTORE_MOCKS, Audition implicitly calls it to ensure mocks are uninstalled before the next test case is executed.

Calling Through to the Original Function 🔗

When a function is mocked, all calls to the original function will be redirected to the mock. The mock may be implemented as a fake or stub, but regardless the original function is never invoked.

Sometimes you do need to call the original function without unregistering the mock with RESTORE or suppressing all mocks with SUSPEND_MOCKS. For these situations, Audition provides the macros CALL and CALL_GET to invoke the original function. The CALL macro lets you invoke the original function and discard any return value it may have. The CALL_GET macro lets you invoke the original function and store its return value in a variable.

The following example uses CALL_GET to invoke the original sum function.

int sum(int x, int y) {
    return x + y;
}

TEST(spam, eggs) {
    STUB(sum, 42);                  // stub sum()
    printf("sum: %d\n", sum(1, 2)); // prints 42

    int result;
    CALL_GET(sum, &result, 1, 2);   // call original sum()
    printf("sum: %d\n", result);    // prints 3
}

You can invoke CALL and CALL_GET macros from a fake function, however, if the function being mocked is invoked recursively then subsequent invocations will not use your fake function. This is because CALL and CALL_GET suspend the mock until they return. It is only after the outermost recursive call returns that the mock will be restored.

Spying Using Fakes 🔗

A spy is a technique used to monitor the behavior of a function, particularly its input arguments and return values, without altering its behavior. Spies let you verify that functions are called with the expected parameters and that they return the correct results.

Audition does not have a built-in spy feature, but you can implement one by redirecting function calls to a fake function. In this fake function, you can assert the input arguments, call the original function, verify the results, and return the actual return value. The following code example demonstrates how to implement spying using this approach. In the example, the original foo function is called, and its return value is transparently passed back. The presence of the fake function remains transparent to the calling code.

int foo(int x); // original function declaration

static int spy_foo(int x) {
    EXPECT_EQ(x, 42);          // assert the argument

    int result;
    CALL_GET(foo, &result, x); // call the original foo()

    EXPECT_EQ(result, 123);    // assert the return value
    return result;
}

TEST(spam, argument_spy) {
    FAKE(foo, spy_foo);  // redirect foo() to spy_foo()
    foo(42);             // transparently calls through spy_foo()
}

In the previous example, the foo function was called directly from the test case, but in real world code, foo might be invoked as part of a hierarchy of function calls. In the call hierarchy, memory and other resources might be allocated that need to be released after foo returns. It is therefore recommended to use the EXPECT_ family of assertion macros in the fake function because these macros do not abort the test case. This lets the function return to the call site for resource cleanup. With the ASSERT_ family of assertion macros, if an assertion fails, it would abort the test thereby leaking any intermediate resources.

Safe Mocking Practices 🔗

While mocking is a powerful tool for isolating tests and controlling the behavior of functions, it comes with certain risks that users should be aware of. Improper use of mocking can lead to unexpected behavior and challenging to diagnose issues.

Mocking system functions, like malloc, can potentially lead to problems, especially when those functions are fundamental to the operation of your code and third-party libraries you depend on. For example:

  • Mocking malloc() - If you stub malloc to return NULL, then any code that relies on malloc will be adversely affected.
  • Mocking exit() - Implementing a fake for exit that does not terminate will lead to code expecting it to terminate to continue executing, likely leading to unintended consequences.

Additionally, some compilers may replace specific C standard function calls with their own implementations in the generated assembly code. At the time of this writing, GCC replaces calls to printf with __printf_chk when optimizations are enabled. In this case mocking printf won’t work because it’s __printf_chk that’s called!

To mitigate these issues, consider limiting your mocks to functions within your application’s business logic rather than core system functions. If you must mock a core system function, then carefully consider its potential impact on the overall system. Similarly, mocking public functions from third-party libraries that your code calls directly is usually fine, but be mindful of any other libraries that invoke those same functions and the impact your mock may have on them.

Wrapping System Functions 🔗

The safest approach to mocking system and third-party library functions is to create a wrapper function around the function you intend to mock, have your code invoke the wrapper, and in your unit tests, instead of mocking the original function, mock the wrapper. By doing so, you limit the scope of the mock to your code, reducing the risk of unintended side effects. For example, if you wanted to mock malloc, wrap it behind a wrapper function, have your application call the wrapper instead of the original malloc function, and then mock the wrapper. This ensures only your application code is affected, while the original function remains intact for use by third-party and system libraries.

Limitations of Mocking 🔗

In Audition, function mocking is implemented by patching the prologue of the target function at runtime to redirect execution to a mock implementation. The ability to patch functions in memory requires an operating system which supports modifying memory protection permissions. Please review the supported platforms and compilers.

When a mock is uninstalled, Audition restores the original prologue.

Working Around Small Functions 🔗

If a function compiles to too few assembly instructions, then it is possible the patched instructions could overwrite the prologue of the next function in memory, thereby breaking it. To avoid this issue, Audition includes a disassembler which verifies the function being mocked has enough assembly instructions to be patched. If a function is too small to be patched, then Audition will gracefully terminate with an error message saying so. So, for example, if a function foo is too small, the message might look something like this:

cannot mock foo(): function implementation too small

If a function is too small to be patched and you can recompile its source code with GCC or Clang, then you can rebuild it with the -fpatchable-function-entry=N flag. This flag instructs the compiler to insert N no-op instructions at the beginning of every function to artificially increase its length. This guarantees no function is too small to mock. Note that there is no best N here because the size of the redirection patch depends upon the instruction set architecture. It’s reasonable to derive N through experimentation.

Beware Compiler Optimizations 🔗

Another caveat with instruction patching is that if an optimizing compiler inlines a function, then it cannot be mocked. For example, compilers may inline static and intrinsic functions like memcpy at their call site. If the function is inlined, then there is no function being invoked at runtime and therefore nothing to patch.

To guarantee all functions are mockable it’s recommended to configure your C compiler to build the code being tested without optimizations. With GCC and Clang this means using the -O0 flag. It’s useful to run tests without optimizations in debug mode anyway with additional tools like address sanitizers to catch potential issues.

If you must run tests with optimizations, then your compiler may provide a mechanism for disabling inlining functions. For example GCC offers the __attribute__((noinline)) attribute and MSVC offers __declspec(noinline). Your compiler might also have a more general flag for disabling inlining altogether.

Depending upon your compiler and linker, you might also be able to circumvent this issue by using a wrapper function declared in a different translation unit than the test case.

// GCC
void __attribute__((noinline)) foo(void)
{
    ...
}

// MSVC
__declspec(noinline) void foo(void)
{
    ...
}

At the time of this writing Clang performs aggressive inlining even if __attribute__((noinline)) and __attribute__((optnone)) are set and even if the -fno-inline flag is used. It is therefore recommended to avoid mocking functions defined in the same compilation unit as the test code. Moving the function to a different compilation unit will “fix” the problem.

At the time of this writing GCC can replace realloc calls with malloc calls if it so chooses. This means if you mock realloc your mock might never be invoked because GCC replaced it with a call to malloc instead. GCC can do this even if no optimizations -O0 flag is provided. This is yet another reason why it is strongly recommended to use wrapper functions for system functions.

Beware Clever Debugging Tools 🔗

Dynamic memory allocation functions like malloc, calloc, and realloc are not mockable when your test runner is executed with Valgrind. This is yet another reason why it is strongly recommended to use wrapper functions for system functions.