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 which are explored in the following sections.
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 match the original function's type signature precisely; 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 can be 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 to 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 can be stubbed with the STUB macro. This macro accepts two arguments: the function to stub and the hard-coded return value. The following example is identical to the fake function example from before, 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 hard-coded return value to precisely match the return type of the function being stubbed. For example, if a function returns an integer, then do not stub it to return a float-point value. If it returns a long
value, then be sure to add the L
suffix to your integer. 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 unmock (remove the mock from) a function using the RESTORE macro. This is useful in situations where you need to revert to the original function 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 mock 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 will never be invoked.
Sometimes you do need to call the original function without unregistering the mock with RESTORE or suppressing all mocks with SUSPEND_MOCKS. In these cases Audition provides the macros CALL and CALL_GET to invoke the original function. The CALL macro allows you to invoke the original function and discard any return value it may have. The CALL_GET macro allows you to invoke the original function and save its return value to a variable.
The following code example invokes both CALL_GET to invoke the original sum
function.
int sum(int x, int y) {
return x + y;
}
TEST(spam, eggs) {
STUB(sum, 42); // mocks 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 go through your fake function. This is because CALL and CALL_GET suspend the mock until they return.
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 concept, however, you can achieve similar functionality by redirecting function calls to a fake function, enabling you to validate both the input arguments and the return value of the original function. The following code snippet demonstrates implementing spying with a fake function. In this example the original foo
function is invoked and its value returned. The fake remains entirely transparent to the calling code.
int foo(int x); // original function declaration
static int fake_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, fake_foo); // redirect foo() to fake_foo()
foo(42); // trigger fake_foo()
}
When spying it is recommended to use the EXPECT_
family of assertion macros because they do not abort the test thereby allowing the fake function to return to the calling code allowing it to perform resource cleanup. So, for example, while we called the foo
function directly from our test in real world code, foo
may be invoked internally as part of a hierarchy of function calls. In the call hierarchy, memory and other resources may be allocated that need to be cleaned after foo
returns. With the ASSERT_
family of assertion macros, if an assertion fails, it would abort the test thereby preventing code being tested from releasing 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 returnNULL
, then any code that relies onmalloc
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.
// 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.