Mock only some specific functions in the same file (partial mocking)
I know this topic has been discussed at length, and several workaround have already been presented, but none of the existing solutions satisfy me fully, and I think Ceedling can do better(?) (although I am not sure that my proposal is feasible).
The workaround that is usually proposed is to move the static functions into private helper file and include it in the main file, so the helper file can be tested separately, and then use the "mocked version" of the helper file when you want to test the main file. But sometimes this is not possible for example when working with legacy code, also if a helper function calls another helper function I have to separate these functions into different files (which I don't like at all), and in any case sometimes I want to keep my static functions in the main file because it makes sense according to the design of my application.
I would like to propose the following solution:
// my_file.c
static int prv_a, prv_b, prv_c;
static int prv_n;
static void prv_foo_N(int n)
{
prv_n = n;
}
static void prv_foo_AB(int a, int b)
{
prv_a = a;
prv_b = b;
prv_foo_N(a + b);
}
void bar(int a, int b, int c)
{
prv_c = c;
prv_foo_AB(a, b);
}
// test_bar_fn.c
/* Tell Ceedling that I want the file "my_file.c" to be included directly in
* this test file.
* So Ceedling could clone the file "my_file.c" into a file with the following
* name "build/test/original_src/src_<this_test_file_name>.c", and then replace
* this macro with:
* #include "build/test/original_src/src_test_bar_fn.c" (yes, .c) */
INCLUDE_SOURCE("my_file.c");
/* Tell Ceedling to literally remove the prv_foo_AB() function from the
* "build/test/original_src/src_test_bar_fn.c" file, and use the "mocked version"
* of it.
* The "mock version" of prv_foo_AB() could be created in a file with the
* following name "build/test/partial_mocks/mock_my_file_c_<function_name>.c"
* which contain only the "mocked version" of prv_foo_AB() function, and then
* replace this macro with:
* #include "build/test/partial_mocks/mock_my_file_c_prv_foo_AB.c" (yes, .c) */
MOCK_FUNCTION("prv_foo_AB");
void test_bar()
{
// Function defined in build/test/partial_mocks/mock_my_file_c_prv_foo_AB.c
prv_foo_AB_Expect(2, 8, 99);
// Function defined in build/test/original_src/src_test_bar_fn.c
bar(2, 9, 99);
TEST_ASSERT_EQUAL_INT(99, prv_n);
}
// test_prv_foo_AB_fn.c
/* Tell Ceedling that I want the file "my_file.c" to be included directly in
* this test file.
* So Ceedling could clone the file "my_file.c" into a file with the following
* name "build/test/original_src/src_<this_test_file_name>.c", and then replace
* this macro with:
* #include "build/test/original_src/src_test_prv_foo_AB_fn.c" (yes, .c) */
INCLUDE_SOURCE("my_file.c");
/* Tell Ceedling to literally remove the prv_foo_N() function from the
* "build/test/original_src/src_test_prv_foo_AB_fn.c" file, and use the
* "mocked version" of it.
* The "mock version" of prv_foo_N() could be created in a file with the
* following name "build/test/partial_mocks/mock_my_file_c_<function_name>.c"
* which contain only the "mocked version" of prv_foo_N() function, and then
* replace this macro with:
* #include "build/test/partial_mocks/mock_my_file_c_prv_foo_N.c" (yes, .c) */
MOCK_FUNCTION("prv_foo_N");
void test_prv_foo_AB()
{
// Function defined in build/test/partial_mocks/mock_my_file_c_prv_foo_N.c
prv_foo_N_Expect(10);
// Function defined in build/test/original_src/src_test_prv_foo_AB_fn.c
prv_foo_AB(2, 8);
TEST_ASSERT_EQUAL_INT(2, prv_a);
TEST_ASSERT_EQUAL_INT(8, prv_b);
}
// test_prv_foo_N_fn.c
/* In this specific case this macro could simply be replaced with:
* #include "my_file.c" (yes, .c), so including the original source file
* without cloning it, because no MOCK_FUNCTION() marco is used in this test
* file. */
INCLUDE_SOURCE("my_file.c")
void test_prv_foo_N()
{
// Function defined in my_file.c
prv_foo_N(10);
TEST_ASSERT_EQUAL_INT(10, prv_n);
}
Basically INCLUDE_SOURCE("file_name.c") tells Ceedling to copy the file file_name.c into build/test/original_src/src_<file_name>.c, so the original_src folder should contain a copy of the original source files from which only the functions specified by the MOCK_FUNCTION("function_name") macros need to be removed.
This eliminates function redefinition errors during compilation and linking.
Would it be possible to implement something like this in Ceedling?
:) What you are describing here is very close to how scalpel works, which is a new CMock feature. It's currently under development, but likely going to be the release after the soon-to-be-released version.
This is great news!!
Could you please link me something about this scalpel, I can't find anything on Google about it.
Sadly at the moment it exists only as a collection of files on my personal repo. It's staged to get added to the next release when some things are worked out.
ok thanks for the info. As far as you know, are there any unit test frameworks out there that support this feature? looking around it seems like no one supports such a thing
Not that I am aware of. As far as I know, we'd be the first... and that makes me excited to work on it. :)
In my opinion this will be one of the features of CMock/Ceedling that will make it stand out from the crowd.
Related issues to collocate in a single issue: #631 #604
I like the idea very much! It will be also useful to have a possibility to mock all the static functions at once. Something like:
MOCK_STATIC_FUNCTIONS();
For legacy codebases this feature will be essential as re-organising an entire codebase to introduce a testing framework is unlikely to fly due to the risk of having to revalidate the entirety of the software. I'm in the middle of doing just that and failing miserably to get a test running on a "module" that has a lot of static functions :(
I am using ceedling for some time now and the missing flexibility of partial mocking and mocking of static functions was a pain at the beginning, but after some time I found a good solution I like to share. I know this is a very long post, but I hope it helps :-)
The solution allows full control over mocking by just one additional helper file (allows also mocking of static functions). The trick is to overwrite the functions by local function pointers by name shadowing. The function pointers point to not implemented functions, but for them cmock creates the mock variants. This allows mixing the original implementation with mocking. For better understanding see the following example:
Example source file containing some static functions that call other static functions:
/*!@file math.c */
#include "math.h"
// Need this in a common.h for example; include common.h in all source files
// redefine static, so that it is removed during testing
#ifdef SW_TEST
#define STATIC
#else
#define STATIC static
#endif
// need this in matching variant for all .c files to be tested
#ifdef SW_TEST
#include "wrap_math.h"
#else
#define MATH_SW_TEST_USE_WRAPPERS
#endif
int getAbs(int val){
return val < 0 ? (val *(-1)) : val;
}
STATIC int getAdc(void){
return 1024;
}
STATIC float getVtg(float vref){
MATH_SW_TEST_USE_WRAPPERS; // remaps getAdc to wrapper variant; the wrapper implementation is created by cmock for us
float vtg = (float)getAdc() / 1024.0f * vref;
return vtg;
}
STATIC int calcTemp(void){
MATH_SW_TEST_USE_WRAPPERS; // remaps getVtg to wrapper variant; the wrapper implementation is created by cmock for us
int temp = (float)getVtg(3.3f) / 2 + 5;
return temp;
}
int getTmp(void){
MATH_SW_TEST_USE_WRAPPERS; // remaps calcTemp to wrapper variant; the wrapper implementation is created by cmock for us
return calcTemp();
}
The following file is the helper file, that provides the trick of shadowing function calls. Just added "wrap_" as prefix. This header file will be included in the test_ file in the mocked variant, so that for the wrap_ declared functions the mocks get created.
/*!@file wrap_math.h */
// Need this additional wrapper file as helper
// Creates a macro that roles out to a list of function pointer mappers
// The local function pointers will shadow external functions
#ifndef WRAP_MATH_H
#define WRAP_MATH_H
// macro that remaps all functions in local scope to use
// the defined wrap_XXXXX methods here, which are never implemented,
// but cmock creates mocking variants for us, so can use mocks for statics;
// this works by defining local function pointers with a name overwriting
// the external original functions
#define MATH_SW_TEST_USE_WRAPPERS int(*getAdc)(void); \
getAdc = &wrap_getAdc; \
float(*getVtg)(float); \
getVtg = &wrap_getVtg; \
int(*calcTemp)(void); \
calcTemp = &wrap_calcTemp; \
int wrap_getAdc(void);
float wrap_getVtg(float vref);
int wrap_calcTemp(void);
#endif /* WRAP_MATH_H */
Test implementation example. The mocks have naming "wrap_getAdc_StubWithCallback" structure now - because of the name remapping. We added prefix wrap_ in the wrapping helper header file, so need this prefix also during test. Using the StubWithCallback mock, we can map to the original function - so have full control in test where to mock and where to call the original function.
/*!@file test_math.c*/
#include "unity.h"
// need original math.h to link the original functions
#include "math.h"
// mock the wrap_math.h ; actually we have no implementation - but we want cmock to create the mocks for us
// to allow testing of static functions and mocking functions in same file - if we want so;
// have also the original math.h included, so that also the original functions are available
#include "mock_wrap_math.h"
// need to declare at least the STATIC functions as extern here!
extern int getAbs(int val);
extern int getAdc(void); // is STATIC
extern float getVtg(float vref); // is STATIC
extern int calcTemp(void); // is STATIC
extern int getTmp(void);
void setUp(void){}
void tearDown(void){}
// callback implemented to use original call instead of mocked variant;
// need this callback only if a real call is necessary
int cb_getAdc(int cnt)
{
return getAdc();
}
void test_getAbs_negVal(void){
int val = -5;
int ret = 1;
// calls the original function
ret = getAbs(val);
TEST_ASSERT_EQUAL_INT(5, ret);
}
void test_getAdc(void){
int ret = 1;
// calls the original function
ret = getAdc();
TEST_ASSERT_EQUAL_INT(1024, ret);
}
void test_getVtg(float vref){
float ret = 1.0f;
wrap_getAdc_ExpectAndReturn(512);
// calls the original function
ret = getVtg(1.0f);
TEST_ASSERT_EQUAL_FLOAT(0.5f, ret);
}
void test_getVtg_originalCall(float vref){
float ret = 1.0f;
// use stub with callback; the callback remaps to
// original implementation, so the function the
// call in test is identical as in real code
wrap_getAdc_StubWithCallback(cb_getAdc);
// calls the original function
ret = getVtg(1.0f);
TEST_ASSERT_EQUAL_FLOAT(1.0f, ret);
}
void test_calcTemp(void){
int ret = 1;
wrap_getVtg_ExpectAndReturn(3.3f, 2);
// calls the original function
ret = calcTemp();
TEST_ASSERT_EQUAL_INT(6, ret);
}
void test_getTmp(void){
int ret = 1;
wrap_calcTemp_ExpectAndReturn(2);
// calls the original function
ret = getTmp();
TEST_ASSERT_EQUAL_INT(2, ret);
}
Basic ceedling project file. Adds testsupport folder, which contains wrapper helpers (here wrap_math.h).
---
#!@file project yml
:project:
:use_exceptions: FALSE
:use_test_preprocessor: :all
:use_auxiliary_dependencies: TRUE
:build_root: testbuild
:test_file_prefix: test_
:which_ceedling: gem
:ceedling_version: 1.0.1
:default_tasks:
- test:all
:enviromnet:
- :path:
- "#{ENV['PATH']}"
:extension:
:executable: .out
:paths:
:test:
- +:test/**
- +:testsupport/**
:source:
- +:src/**
:include:
- +:src/**
:support: []
:libraries: []
:defines:
:common: &common_defines []
:test:
- *common_defines
- SW_TEST
:test_preprocess:
- *common_defines
- SW_TEST
:cmock:
:mock_prefix: mock_
:when_no_prototypes: :warn
:enforce_strict_ordering: TRUE
:treat_externs: :include
:plugins:
- :ignore
- :callback
- :expect_any_args
- :ignore_arg
- :return_thru_ptr
:treat_as:
uint8_t: HEX8
uint16_t: HEX16
uint32: UINT32
int8: INT8
bool: UINT8
...
The original header file for math.c for completeness.
/*!@file math.h */
#ifndef MATH_H
#define MATH_H
/**@brief function to get the abs value of given int */
int getAbs(int val);
int getTmp(void);
#endif /* MATH_H */
ceedling clean clobber test:all
And hopefully get no failures and enjoy testing :-)
Flo1991
It's a smart way to mock static functions, thanks for sharing, same approach can be used to mock inline functions.
That said, I’d prefer not to clutter the codebase with test-specific code, we aim to keep it focused solely on production logic. Hopefully, CMock/Ceedling will eventually support this functionality natively.
Example project based on your approach by adding the mocking of inline function:
ceedling_static_and_inline_mocking.zip
Any due date for this feature?