如何在C语言中使用Google Mock?

23

我正在维护一个用C语言编写的遗留项目,但是使用C++编译器无法运行它。由于代码是交叉编译的,因此可以在主机环境中运行单元测试或类似的东西。因此,也可以与C++主机编译器进行接口,使用google-test和google-mock。

Google-mock具有一些非常诱人的功能,例如调用实际的实现并设置调用期望值,这些功能似乎很适合用于测试。

我想要能够在C代码中使用它们。 我可以看到确实可以在不使用虚函数表的情况下使用google-mock,但是需要模板。

是否有一种方法可以使用google mock模拟裸C函数?

编辑:

我基本上必须使用google mock,但我假设每个阅读此线程的人都比我更具灵活性。


我很好奇在C项目中,哪些内容无法使用C++编译器进行编译。 - Carey Gregory
2
@carygregory 有时候,简单的事情就是 foobar * x = malloc(sizeof(foobar)); 而不是将其强制转换为 (foobar *)malloc(sizeof(foobar)); - Alexander Oh
2
当你将C代码移植到C++时,通常需要添加大量的强制类型转换(例如,对于所有调用malloc的函数以及任何地方,你不能再将void*隐式转换为一个类型化指针)。 - Paul R
我认为在C语言中,当枚举类型使用++运算符时,会将变量推进到枚举的下一个值。而在C++中,它只是加上1。我相信还有其他的区别。 - HeywoodFloyd
3
@HeywoodFloyd 使用 ++ 操作枚举类型的人可能会遭到应有的下场。;-) - Carey Gregory
1
@CareyGregory,此外,谁说目标平台有C++编译器。我们有8051和其他嵌入式架构。 - Alexander Oh
4个回答

16

我找到了一种方法可以在google-mock中模拟裸的C函数。

解决方法是将 foobar 声明为弱别名,映射到 foobarImpl 上。在生产代码中,您不需要实现 foobar(),而在单元测试中,您提供一个调用静态模拟对象的实现。

这个解决方法只适用于GCC,但还有其他编译器/链接器提供弱别名功能。

  • 将函数void foobar();重命名为void foobarImpl();
  • 向函数foobar添加一个属性,如:void foobar() __attribute__((weak, alias("foobarImpl") ));
  • 如果您想要一个非弱别名,请使用预处理指令从属性中删除weak。

因此:

#pragma once
void foobar();

成为

// header.h
#pragma once

void foobar();    
void foobarImpl(); // real implementation

extern "C" {
#include "header.h"
}
// code.c
void foobarImpl() {
  /* do sth */
}
void foobar() __attribute__(( weak, alias ("foobarImpl") )); // declare foobar to be a weak alias of foobarImpl

这将告诉GNU链接器在没有名为foobar()的符号时,将foobar()的调用与foobarImpl()链接起来

然后添加测试代码

struct FooInterface {
   virtual ~FooInterface() {}
   virtual void invokeFoo() const { }
};

class MockFoo : public FooInterface {
public:
  MOCK_CONST_METHOD0(invokeFoo, void());
}

struct RealFoo : public FooInterface {
   virtual ~RealFoo() {}
   virtual void invokeFoo() const { foobarImpl(); }
};

MockFoo mockFoo;
RealFoo realFoo;
void foobar() {
  mockFoo.invokeFoo();
}

如果编译和链接此代码,则将使用模拟调用替换foobar。 如果您真的想调用foobar(),仍然可以添加默认调用。

ON_CALL(mockFoo, invokeFoo())
       .WillByDefault(Invoke(&realFoo,&RealFoo::invokeFoo));

2
很高兴看到你找到了一种可行的方法来封装C API函数。这种解决方案/变通方法正是我所想到的... - πάντα ῥεῖ
我对这个答案有些难以理解。也许如果您能 1. 解释一下 host 和 target 是什么意思(原 C 函数、重命名的 C++ 替代品?),2. 更正未声明的 FooImpl 的使用,3. 说明为什么没有在任何地方提到 extern "C",4. 解释提到的强别名在哪里使用 - 我只能找到一个弱别名的声明,并提供将其变为强别名的选项。 - yau
@yau,谢谢你的评论。我会修改答案,但是如果你看一下问题,这是关于交叉编译C代码的,因此针对目标/主机的措辞是该场景的标准名称。我可以修改答案,使其更普遍适用。第3点是标准要求的,但这是GCC特定的解决方案,gcc不会抱怨缺少mangling声明。第4点没有强别名声明,强别名只是链接器在看到重复符号时的默认行为。我会修改答案,不再使用“strong alias”这个措辞。 - Alexander Oh
这里有一个关于gcc目标/主机的答案:https://dev59.com/Z2w05IYBdhLWcg3wszwl。我没有在任何地方放置`extern "C"`,因为代码仍然是使用真正的C编译器编译的,我不想用C++结构来混淆C代码,并使用预处理器再次将它们删除。 - Alexander Oh

3

根据Google Mock的常见问题解答:

我的代码调用了一个静态/全局函数,我能对它进行模拟吗?
可以模拟,但需要做一些更改。

一般来说,如果你发现自己需要模拟一个静态函数,这意味着你的模块耦合度过高(灵活性、可重用性、可测试性等都会降低)。你最好定义一个小接口,并通过该接口调用函数,然后可以轻松地对其进行模拟。虽然最初需要一些工作,但通常很快就会回报。

这篇Google Testing Blog 文章讲得非常好,请查看。


3
那么那就是C++了。不幸的是,非测试代码需要保持为C语言。 - Alexander Oh
@Alex 尽管如此,这就是GoogleMock提供的全部!其他任何东西都需要适配器/包装器。 - πάντα ῥεῖ

3
您的问题明确提到了Google Mock,但随后并未说明使用该框架的其他原因。另一个答案建议使用一种似乎过于侵入式的解决方法。
因此,我希望可以提出一种替代建议,它可以在不必使用弱别名等任何诡计的情况下很好地工作。
我曾经在几个大型主要为C语言编写(有些是C++)的项目中,使用CppUTest(https://cpputest.github.io/)进行单元测试和模拟,取得了成功。模拟工作正常,无需采用上述任何诡计手段。
不幸的是,该项目文档有点薄弱,一些更好的(如果有点敏捷信条)信息和示例可以在书籍《嵌入式C测试驱动开发》(也可作为PDF文件流传) - James W Greening(ISBN-13:978-1-934356-62-3)中找到。

我们使用Google Mock的原因是有其自身的优势,而使用其他测试框架则不是一个选择。当然,提出替代方案是可以的,但并非每个人都能够采用这些替代方案(比如假设已经有20万个C++ GMock测试用例,你会转移到CppUTest吗?)。在提出替代方案时,请给出一个最小的工作示例来展示其优点。 - Alexander Oh
我非常理解这一点,这就是为什么我在回答问题时如此犹豫的原因!由于问题没有要求提供竞争性的例子,所以我没有去挖掘我的2014年的代码并创建一些例子,抱歉 - 我认为只提供指向替代方案的指针就足够了,在没有要求实际替代方案的情况下!我非常谨慎地给出不成立的“答案”,在这种情况下,提问者询问“框架X”,而回答者则说“你尝试过框架Y吗?”在这种情况下,我认为值得提及,但不必深入探讨。 - MikeW
1
如果答案对那些发现这个问题的人有帮助,我很乐意点赞。并不是每个人都在寻找我过去所寻找的相同答案。我不能再对那段代码进行更改了。 - Alexander Oh
1
如果时间允许,我可能会添加一个示例。 - MikeW

2

我知道这是一个非常旧的帖子,但我希望如果有人在遇到这个问题时,我可以让他们的生活变得更容易一些。

你可以使用Mimicc很容易地为与GoogleTest兼容的C函数自动生成mocks。找到声明所需mock函数的头文件,“编译”它们成为mock实现对象文件,并将它们链接到测试二进制文件中,包括mock_fatal()mock_failure()函数的定义,如用户指南中特别针对Google Test所述。您将需要使用Mimicc API与Mimicc生成的mocks进行交互(即,它不使用GoogleMock的API设置期望等),但它们可以舒适地与GoogleMock生成的mocks并存。

更具体地说,假设您有一个C头文件foo.h,其中声明了您想要mock的几个函数。例如:

/*!
 * @param[out] pBuf Destination buffer to read to
 * @param[in] sz Size of the read buffer
 */
int magic_read(char *pBuf, const size_t sz);

/*!
 * @param[in] pBuf Source buffer to write from
 * @param[in] sz Size of the write buffer
 */
int magic_write(const char *pBuf, const size_t sz);

你可以通过使用与编译生产环境中的 foo.c 相同的所有CFLAGS来编译 foo.h,从而为它们创建模拟。
prompt$ mimicc -c foo.h -o mock.o --hout=foo-mock.h -DSOME_PREPROC=1 -I <your includes>

要在测试中使用此功能,请按照上面命令行调用中所示,使用foo-mock.h中声明的API设置期望值和返回值。为Google Test包含mock_fatal()mock_failure()的实现。

#include <gtest/gtest.h>
#include <memory>

std::unique_ptr<char []> mockErrStr(const char *pLocation, unsigned count, const char *pMsg)
{
    const char *pFmtStr = "mock assertion failure! location: '%s',"
                          " iteration: %d, message: %s";
    size_t n = snprintf(NULL, 0, pFmtStr, pLocation, count, pMsg);
    std::unique_ptr<char []> outStrBuf(new char[n+1]);
    snprintf(outStrBuf.get(), n+1, pFmtStr, pLocation, count, pMsg);
    return outStrBuf;
}

void mock_failure(const char *pLocation, unsigned count, const char *pMsg)
{
    ADD_FAILURE() << mockErrStr(pLocation, count, pMsg).get();
}

void mock_fatal(const char *pLocation, unsigned count, const char *pMsg)
{
    FAIL() << mockErrStr(pLocation, count, pMsg).get();
    exit(1);
}

TEST_F(MimiccPoC, test1)
{
    char mock_ret_data[] = "HELLO WORLD";
    MOCK_FUNCTIONS(foo).magic_read.expect(32);
    MOCK_FUNCTIONS(foo).magic_read.andReturn(
            1, mock_ret_data, sizeof(mock_ret_data));

    char testRetBuf[32];
    int testRes = magic_read(testRetBuf, sizeof(testRetBuf));
    ASSERT_EQ(1, testRes);
    ASSERT_STREQ(testRetBuf, "HELLO WORLD");
}

虽然这听起来很多,但一旦管道设置好了,你就可以自动模拟任何C或C++代码,而无需编写或维护额外的模拟代码,你只需要专注于测试。从长远来看,这会更加容易。


说实话,任何能帮助别人且比我这个半个十年前的解决方案更现代化的东西都应该被视为有价值的贡献。让我阅读并检查一下,一旦我花时间了就会给你点赞。谢谢。 - Alexander Oh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接