Given a C library (liblegacy.a
) that contains:
function_legacy1()
function_legacy2()
function_legacy3()
...
function_legacy500()
and a C binary (mybin
) which links against liblegacy.a
:
function_binary1() {
function_legacy1();
function_legacy2();
function_legacy3();
}
function_binary200() {
function_legacy500();
}
and mybin
is partially tested with Google Test framework (in progress).
The technical debt is high and it will be a big work to test mybin
and/or liblegacy.a
.
As a step to remove this debt, I like to start implementing a test for function_binary1
without impacting the rest. My idea would be to mock only the 3 functions used by it (function_legacy1
, function_legacy2
, function_legacy3
) and keep linking against the lib so that I don't have to split the .c/.h files to have only the interesting part in the translation unit.
A first approach would probably to make a dynamic shared library that contains the 3 functions, and use LD_PRELOAD to override these ones at runtime.
Since I'm new with GMock, maybe we can do that in a better way directly with this framework. Is is possible to mock only some functions of an external lib with GMock to avoid refactoring in this case?
Note: This question is somehow related to, but answers are not very clear to me
Can gmock be used for stubbing C functions?
CodePudding user response:
You might run into problems if you are working with a static library
(liblegacy.a
) and a shared library libmock.so
(libmock.dylib
on
macOS) and if any of the mocked functions is defined in an object file
that also contains an unmocked function.
For example, if you mock function_legacy1()
but in liblegacy.a
it is
defined in the same object file as function_legacy2()
, you might run
into linking conflicts with function_legacy1()
being multiply defined,
or you might run into the wrong code being executed at runtime.
Consider the following setup:
legacy.h
— declares the legacy functions.legacy_1_2.c
— definesfunction_legacy1()
andfunction_legacy2()
.legacy_3_4.c
— definesfunction_legacy3()
andfunction_legacy4()
.mock_1.c
— definesfunction_legacy1()
.mock_2.c
— definesfunction_legacy2()
.testprog1.c
— defines amain()
function that calls each of the legacy functions.testprog2.c
— a copy oftestprog1.c
.
Each of the legacy functions in each source file is the same apart from the name:
void function_legacy1(void)
{
printf("%s:%d:%s()\n", __FILE__, __LINE__, __func__);
}
The main()
program looks like:
#include "legacy.h"
#include <stdio.h>
int main(void)
{
printf("%s:%d:%s()\n", __FILE__, __LINE__, __func__);
function_legacy1();
function_legacy2();
function_legacy3();
function_legacy4();
printf("%s:%d:%s()\n", __FILE__, __LINE__, __func__);
return 0;
}
The code can be compiled and run as shown below:
$ gcc -shared -o libmock1.dylib mock_1.c
$ gcc -shared -o libmock2.dylib mock_1.c mock_2.c
$ gcc -o testprog1 testprog1.c -L. -lmock1 -llegacy
$ gcc -o testprog2 testprog2.c -L. -lmock2 -llegacy
$ testprog1
testprog1.c:6:main()
legacy_1_2.c:6:function_legacy1()
legacy_1_2.c:11:function_legacy2()
legacy_3_4.c:6:function_legacy3()
legacy_3_4.c:11:function_legacy4()
testprog1.c:11:main()
$ testprog2
testprog2.c:6:main()
mock_1.c:6:function_legacy1()
mock_2.c:6:function_legacy2()
legacy_3_4.c:6:function_legacy3()
legacy_3_4.c:11:function_legacy4()
testprog2.c:11:main()
$
As you can see, there are no problems linking.
However, testprog1
is linked with a mock library that only includes a
mock for function_legacy1()
, and it doesn't see the mocked version of
function_legacy1()
.
By contrast, testprog2
is linked with a mock library that includes a
mock for function_legacy1()
and function_legacy2()
and it does see
both the mocked functions.
YMMV on a Linux system, or other non-macOS systems.
Thus, your mocking may or may not work if the legacy functions are defined several per object file in the static library.
CodePudding user response:
Sounds like you want to conditionally delegate some functions to the real implementation (function_legacy1
, function_legacy2
, function_legacy3
) and the rest to the mock.
I think you should be able to use this recipe.
For mocking stand-alone functions such as function_legacy1
, you probably need to wrap them inside a virtual class first. Something like this:
class LegacyInterface {
public:
...
virtual bool function_legacy1(int p) = 0;
virtual bool function_legacy2(int p) = 0;
...
virtual bool function_legacyn(int p) = 0;
};
// This should be used in production.
class LegacyProduction : public LegacyInterface {
public:
bool function_legacy1(int p) override {
return ::function_legacy1(p); // Calling the real function_legacy1
}
bool function_legacy2(int p) override {
return ::function_legacy2(p); // Calling the real function_legacy2
}
...
bool function_legacyn(int p) override {
return ::function_legacyn(p); // Calling the real function_legacyn
}
};
// This should be used in test.
class LegacyMock : public LegacyInterface {
public:
// Normal mock method definitions using gMock.
MOCK_METHOD(bool, function_legacy1, (int p), (override));
MOCK_METHOD(bool, function_legacy2, (int p), (override));
...
MOCK_METHOD(bool, function_legacyn, (int p), (override));
// Delegates the default actions of function_legacy500 to the real implementation.
void Delegate500ToReal() {
ON_CALL(*this, function_legacy500).WillByDefault([this](int n) {
return ::function_legacy500(n); // Calling the real function_legacy500
});
}
};
Now you should change your code to use the wrapper class:
function_binary1(LegacyInterface *legacy_wrapper) {
legacy_wrapper->function_legacy1();
legacy_wrapper->function_legacy2();
legacy_wrapper->function_legacy3();
}
function_binary200(LegacyInterface *legacy_wrapper) {
legacy_wrapper->function_legacy500();
}
where you initialize legacy_wrapper for test:
LegacyInterface *legacy_wrapper = new LegacyMock;
And for production:
LegacyInterface *legacy_wrapper = new LegacyProduction;
So your test would be:
TEST(LegacyTest, function_binary1) {
LegacyMock legacy_wrapper;
EXPECT_CALL(foo, function_binary1(_));
EXPECT_CALL(foo, function_binary2(_));
EXPECT_CALL(foo, function_binary3(_));
// Assert
EXPECT_EQ(function_binary1(&legacy_wrapper), ...);
}
TEST(LegacyTest, function_binary200) {
LegacyMock legacy_wrapper;
legacy_wrapper.Delegate500ToReal();
// No action specified, meaning to use the default action, which is calling the real function_legacy500.
EXPECT_CALL(foo, function_legacy500(_));
// Assert
EXPECT_EQ(function_binary200(), ...);
}
The above code can still be linked against the legacy lib as was before.