ThrowTheSwitch / Ceedling

Ruby-based unit testing and build system for C projects
http://throwtheswitch.org
Other
585 stars 246 forks source link

Mock only some specific functions in the same file (partial mocking) #936

Open parmi93 opened 6 days ago

parmi93 commented 6 days ago

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?

mvandervoord commented 6 days ago

:) 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.

parmi93 commented 6 days ago

This is great news!! Could you please link me something about this scalpel, I can't find anything on Google about it.

mvandervoord commented 6 days ago

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.

parmi93 commented 6 days ago

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

mvandervoord commented 6 days ago

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. :)

parmi93 commented 5 days ago

In my opinion this will be one of the features of CMock/Ceedling that will make it stand out from the crowd.