Google 在 Linux 上模拟一个自由系统函数总是以内存泄漏结束

Google Mock a free system function on Linux always finishes with memory leak

我正在尝试模拟 Linux 标准库中的一个简单函数。 strerror() returns 来自 errno 的错误信息。这是我的库,具有模拟功能:

~$ cat mylib.c
#include <string.h>
#include <stdio.h>

int myStrerror()
{
    int error_number = 0;

    char* buffer = strerror(error_number);
    fprintf(stdout, "Returned string =  '%s'\n", buffer);
    return 0;
}

#if defined (EXECUTABLE)
int main(int argc, char **argv)
{
    return myStrerror();
}
#endif

~$ g++ -pedantic-errors -Wall -c mylib.c

这是我的 google 测试:

~$ cat test_mylib.cpp
#include "gtest/gtest.h"
#include "gmock/gmock.h"

int myStrerror();

class strerrorMock {
public:
    MOCK_METHOD(char*, strerror, (int));
};

strerrorMock strerrorMockObj;

char *strerror(int error_number) {
    return strerrorMockObj.strerror(error_number);
}

TEST(MockTestSuite, strerror)
{
    using ::testing::Return;

    char response[] = "mocked strerror function";

    EXPECT_CALL(strerrorMockObj, strerror(0))
        .WillOnce(Return(response));
    EXPECT_EQ(myStrerror(), 0);
    ::testing::Mock::VerifyAndClearExpectations(&strerrorMockObj);
}


int main(int argc, char **argv) {
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

~$ g++ -pedantic-errors -Wall \
        -o test_mylib.a \
        -I"$BUILD_DIR"/googletest-src/googletest/include \
        -I"$BUILD_DIR"/googletest-src/googlemock/include \
        test_mylib.cpp \
        "$BUILD_DIR"/lib/libgtestd.a \
        "$BUILD_DIR"/lib/libgmockd.a \
        ./mylib.o \
        -lpthread

这是它通常 returns 的样子:

~$ ./mylib.a
Returned string = 'Success'

和 运行 测试给出了这个:

~$ ./test_mylib.a
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from MockTestSuite
[ RUN      ] MockTestSuite.strerror
Returned string = 'mocked strerror function'
[       OK ] MockTestSuite.strerror (0 ms)
[----------] 1 test from MockTestSuite (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.

test_mylib.cpp:32: 错误:这个模拟对象(在测试 MockTestSuite.strerror 中使用)应该被删除,但从来没有被删除。它的地址是@0x56114aa239e0.
错误:在程序退出时发现 1 个泄漏的模拟对象。当对象被破坏时,对模拟对象的期望得到验证。泄漏模拟意味着它的期望没有得到验证,这通常是一个测试错误。如果你真的打算泄漏模拟,你可以使用 testing::Mock::AllowLeak(mock_object) 来抑制这个错误,或者你可以使用假的或存根而不是模拟。

我该怎么做才能避免内存泄漏?

问题是我们使用了系统库中的免费全局函数strerror()。它并没有像往常一样被 Googlemock 的界面真正模拟。所以我们不需要接口。我们还必须用一个自由函数来覆盖模拟函数,该函数必须是全局的,才能与系统函数在同一范围内,因为它将替换它。这就是我们所做的:

strerrorMock strerrorMockObj;

char* strerror(int error_number) {
    return strerrorMockObj.strerror(error_number);
}

这里 mock 的实例 strerrorMockObj 也在全局范围内,可以在函数内调用。但显然 Googletest 无法删除错误消息中指出的全局模拟对象。我发现的一种解决方案是像往常一样在测试宏中实例化模拟对象并存储一个指向它的全局指针,以便函数可以寻址它:

strerrorMock* ptrStrerrorMockObj;
char* strerror(int error_number) {
    return ptrStrerrorMockObj->strerror(error_number);
}

TEST(MockTestSuite, strerror)
{
    strerrorMock strerrorMockObj;
    ptrStrerrorMockObj = &strerrorMockObj;
...
}

然后没有抱怨内存泄漏的完整测试程序如下所示:

~$ cat test_strerror.cpp
#include "gtest/gtest.h"
#include "gmock/gmock.h"

int myStrerror();

class strerrorMock {
public:
    MOCK_METHOD(char*, strerror, (int));
};

strerrorMock* ptrStrerrorMockObj;
char* strerror(int error_number) {
    return ptrStrerrorMockObj->strerror(error_number);
}

TEST(MockTestSuite, strerror)
{
    using ::testing::Return;

    strerrorMock strerrorMockObj;
    ptrStrerrorMockObj = &strerrorMockObj;

    char mockedstr[] = "mocked strerror function";
    EXPECT_CALL(strerrorMockObj, strerror(0))
        .WillOnce(Return(mockedstr));
    EXPECT_EQ(myStrerror(), 0);
}

int main(int argc, char **argv) {
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

在几个平台上使用我的第一个答案的解决方案一个月后,我发现它不是很稳定。特别是在 MS Windows 上,我遇到了麻烦,因为 GoogleMock 并不总能找到模拟功能。所以我决定接受对生产代码的最小修改,并按照 googletest 的建议使用 a wrapper class for the free system functions

下面我只需要在生产代码中添加一个头文件并更改其系统调用,例如

# from
fd = fopen("openclose.txt", "a");
# to
fd = stdioif->fopen("openclose.txt", "a");

在 Microsoft Windows 上,我从 github 克隆了 googletest 使用 powershell 构建它并设置 cmake -S . -B build 然后 cmake --build build --config MinSizeRel 并使用此结构保留在其根目录中:

├── build
│   └── lib
│       └── MinSizeRel
│           ├── gmock.lib
│           ├── gmock_main.lib
│           ├── gtest.lib
│           └── gtest_main.lib
├── include
│   └── stdioif.h
├── src
│   ├── main.cpp
│   ├── openclose.cpp
│   └── test_openclose.cpp
├── main.exe
├── main.obj
├── openclose.txt
├── test_openclose.exe
└── test_openclose.obj

这是头文件:

#ifndef INCLUDE_STDIOIF_H
#define INCLUDE_STDIOIF_H

#include <stdio.h>

class Istdio {
// Interface to stdio system calls
  public:
    virtual ~Istdio() {}
    virtual FILE* fopen(const char* pathname, const char* mode) = 0;
    virtual int fprintf(FILE* stream, const char* format) = 0;
    virtual int fclose(FILE* stream) = 0;
};


// Global pointer to the  current object (real or mocked), will be set by the
// constructor of the respective object.
Istdio* stdioif;


class Cstdio : public Istdio {
// Real class to call the system functions.
  public:
    virtual ~Cstdio() {}

    // With the constructor initialize the pointer to the interface that may be
    // overwritten to point to a mock object instead.
    Cstdio() { stdioif = this; }

    FILE* fopen(const char* pathname, const char* mode) override {
        return ::fopen(pathname, mode);
    }

    int fprintf(FILE* stream, const char* format) override {
        return ::fprintf(stream, format);
    }

    int fclose(FILE* stream) override {
    }
};

// This is the instance to call the system functions. This object is called
// with its pointer stdioif (see above) that is initialzed with the
// constructor. That pointer can be overwritten to point to a mock object
// instead.
Cstdio stdioObj;

/*
 * In the production code you must call it with, e.g.:

    stdioif->fopen(...)

 * The following class should be coppied to the test source. It is not a good
 * idea to move it here to the header. It uses googletest macros and you always
 * hove to compile the code with googletest even for production and not used.

class Mock_stdio : public Istdio {
// Class to mock the free system functions.
  public:
    virtual ~Mock_stdio() {}
    Mock_stdio() { stdioif = this; }
    MOCK_METHOD(FILE*, fopen, (const char* pathname, const char* mode), (override));
    MOCK_METHOD(int, fprintf, (FILE* stream, const char* format), (override));
    MOCK_METHOD(int, fclose, (FILE* stream), (override));
};

 * In a gtest you will instantiate the Mock class, prefered as protected member
 * variable for the whole testsuite:

    Mock_stdio mocked_stdio;

 *  and call it with: mocked_stdio.fopen(...) (prefered)
 *  or                    stdioif->fopen(...)
*/

#endif // INCLUDE_STDIOIF_H

这是简单的示例程序:

#include "stdioif.h"

#include <iostream>

int openclose() {
    FILE* fd = nullptr;
    int rc = 0;

    fd = stdioif->fopen("openclose.txt", "a");
    if(fd == NULL) {
        std::cerr << "Error opening file\n";
        return 1;
    }

    rc = stdioif->fprintf(fd, "hello world :-)\n");
    if(rc < 0) {
        std::cerr << "Error appending to file with return code: " << rc << "\n";
        stdioif->fclose(fd);
        return rc;
    }

    rc = stdioif->fclose(fd);
    if(rc) {
        std::cerr << "Error closing file with return code: " << rc << "\n";
        return rc;
    }

    std::cout << "done.\n";
    return 0;
}

我执行它:

#include "src/openclose.cpp"

int main() {
    return openclose();

测试程序如下所示:

#include "gtest/gtest.h"
#include "gmock/gmock.h"

#include "stdioif.h"

#include "src/openclose.cpp"

using ::testing::_;
using ::testing::Return;


class Mock_stdio : public Istdio {
// Class to mock the free system functions.
  public:
    virtual ~Mock_stdio() {}
    Mock_stdio() { stdioif = this; }
    MOCK_METHOD(FILE*, fopen, (const char* pathname, const char* mode), (override));
    MOCK_METHOD(int, fprintf, (FILE* stream, const char* format), (override));
    MOCK_METHOD(int, fclose, (FILE* stream), (override));
};


class OpenCloseTestSuite: public ::testing::Test {
  protected:
    // Member variables of the whole testsuite: instantiate the mock objects.
    Mock_stdio mocked_stdio;
};


TEST_F(OpenCloseTestSuite, open_close) {

    EXPECT_CALL(mocked_stdio, fopen(_, _))
        .WillOnce(Return((FILE*)0x123456abcdef));

    EXPECT_CALL(mocked_stdio, fprintf(_,_))
        .WillOnce(Return(-1));

    EXPECT_CALL(mocked_stdio, fclose(_)).Times(1);

    // process unit
    EXPECT_EQ(openclose(), 0);
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

要在 Microsoft Windows 上编译它,我使用:

cl -nologo  /EHsc -I. -I.\include -I.\googletest\include -I.\googlemock\include .\build\lib\MinSizeRel\gtest.lib .\build\lib\MinSizeRel\gmock.lib .\src\[main.cpp | test_openclose.cpp]