【问题标题】:Writing unit-test and need to control the c++ linker编写单元测试并需要控制 c++ 链接器
【发布时间】:2021-03-29 11:40:13
【问题描述】:

我正在为依赖于第 3 方 C 和 C++ 库以及第 1 方 C 库的 C++ 类编写单元测试。我遇到了麻烦,因为我希望被测类使用模拟的第 3 方 C 库,而测试运行程序使用真正的第 3 方 C 库。

  1. 我正在编写单元测试,而被测类依赖于libx
  2. 我创建了一个libmockx,它允许我测试参数并注入返回值。
  3. 被测类需要链接到libmockx,以便我可以检查和控制它的行为。
  4. 单元测试应用程序,需要链接到libx 才能制定/解析libx 数据类型。

使用哪种模式或方法将测试运行器链接到libx,将被测类链接到libmockx,然后将测试运行器链接到被测类?许多解决方案都在讨论让链接器做“脏活”,但ld 有 100 个参数,我不知道如何让它工作。

目前,我对所有模拟实现都有重新定义错误,我需要一种解决方法(无论是否涉及链接器)。

在下面编辑: (回应 cmets)

想象 7 个文件:

  • test.cpp - 测试运行者
  • object.cpp - 正在测试的object
  • object.hpp - object 标头
  • mock-parameters.h - 提供对测试的模拟参数访问
  • mock-x.c - x 的模拟实现
  • x.c - x 实现
  • x.h - x 标头

在我需要实例化和操作x 对象之前,我能够通过对g++ 的一次调用来编译我的测试:

g++ test.cpp object.cpp mock_x.c

我正在尝试向test.cpp 添加一个测试,它将提供和测试x 对象结果值。现在,我需要将test.cppx.c 链接,同时仍将object.cppmock_x.c 链接。

当我将x.c 添加到编译列表时,我收到以下(预期的)错误:

/usr/bin/ld: /tmp/ccDVQxtN.o: in function `set_foo(X*, int)':
mock-x.c:(.text+0x0): multiple definition of `set_foo(X*, int)'; /tmp/cc3t8CjP.o:x.c:(.text+0x14): first defined here
collect2: error: ld returned 1 exit status

x.h

#ifndef X_H
#define X_H

typedef struct X {
    int foo;
    char bar;
} X;

X * create_x (void);
int set_foo (X *, int);
char set_bar (X *, char);
void delete_x (X *);

#endif // X_H

x.c

#include "x.h"

#include "stdlib.h"

X * create_x (void) {
    return (X *)malloc(sizeof(X));
}

int set_foo (X * x, int i) {
    x->foo = i;
    return i;
}

char set_bar (X * x, char c) {
    x->bar = c;
    return c;
}

void delete_x (X * x) {
    free(x);
}

mock-x.c

#include "x.h"
#include "mock-parameters.h"

Set_foo_params set_foo_params;

int set_foo (X * x, int i) {
    // Stash parameter(s)
    set_foo_params.x = x;
    set_foo_params.i = i;

    return set_foo_params.result;
}

object.hpp

#ifndef OBJECT_HPP
#define OBJECT_HPP

#include "x.h"

class Object {
    public:
    void embed_x(X *);
    int increment_foo(int);

    private:
    X * _x;
};

#endif // OBJECT_HPP

object.cpp

#include "object.hpp"

void Object::embed_x (X * x) {
    _x = x;
}

int Object::increment_foo (int i) {
    ++i;
    return set_foo(_x, i);
}

mock-parameters.h

#include "x.h"

typedef struct Set_foo_params {
    X * x;
    int i;
    int result;
} Set_foo_params;

extern Set_foo_params set_foo_params;

test.cpp

#include "object.hpp"
#include "mock-parameters.h"

int test_object_update_foo_correctly_invokes_x_set_foo (void) {
    int result;
    Object object;

    // Setup
    X * x = create_x();
    object.embed_x(x);

    // Execute
    object.increment_foo(7);

    // Test
    if (8 == set_foo_params.i) {
        result = 0;
    } else {
        result = 1;
    }

    delete_x(x);
    return result;
}

int test_object_update_foo_correctly_returns_x_set_foo_result (void) {
    int result;
    Object object;

    // Setup
    X * x = create_x();
    object.embed_x(x);
    set_foo_params.result = 5;

    // Execute
    int output = object.increment_foo(0);

    // Test
    if (5 == output) {
        result = 0;
    } else {
        result = 2;
    }

    delete_x(x);
    return result;
}

int main (void) {
    int result;

    result |= test_object_update_foo_correctly_invokes_x_set_foo();
    result |= test_object_update_foo_correctly_returns_x_set_foo_result();

    return result;
}

【问题讨论】:

  • 建议将您正在使用的构建工具添加到问题中。它将直接影响所提供的解决方案。
  • 我现在正在手工编译。想象一下 6 个文件,test.cppobject.cppmock_x.cx.cobject.hppx.h
  • 在这种情况下,您只需要在命令行上指定不同的库。也许在问题中添加您当前正在做什么的示例会动摇松散的答案。
  • 只是一个想法:使用--wrap 选项拦截对库的所有调用怎么样?您可以使用外部标志来决定在包装函数中模拟函数或调用真实函数。 -- 请提供minimal reproducible example 好吗?
  • object.c 中使用的 API(具有不同的 x.c 和 mock-x.c 实现)的功能和 test.cpp 中使用的功能是否重叠?

标签: c++ c unit-testing mocking linker


【解决方案1】:

既然您澄清了您的 object.ctest.c 使用相同的功能 - 但应该使用不同的实现 - 唯一的方法是将这些功能分开大大地。根据 ODR(C++ 编程语言的一个定义规则),你不能对同一个函数/对象有两个不同的实现。

所以唯一的方法是创建两个单独的函数/对象(具有单独的名称)。您可以使用不同的名称或将相同的名称放在不同的命名空间中。

例子:

x.h

#ifndef X_H
#define X_H

#ifdef X_DEBUG
#define X mock_x
#else
#define X x
#endif

namespace X {

int foo();

}

#endif // X_H

x.cpp

namespace x {

int foo()
{
    int res = -1;
    // rightful implementation
    return res;
}

}

mock-x.cpp

namespace mock_x {

int foo()
{
    int res = -1;
    // mock implementation
    return res;
}

}

object.cpp

#define X_DEBUG
#include <x.h>

int obj_bar()
{
    int res = X::foo(); // uses mock implementation
    return res;
}

}

test.cpp

#include <x.h>

int test_bar()
{
    int res = X::foo(); // uses real implementation
    return res;
}

}

显然这种方法是可扩展的——您可以在这个命名空间 X 中放置任意数量的类/函数/对象。通用功能可以在标题 中内联定义,而在相应模块 x.cmock-x.c 中单独定义功能。

一般:

我肯定会着眼于将 mock-x 实现创建为一组独特的链接器符号。这绝对是更加可控和灵活的方法。你甚至可以从你的 mock-x 功能中使用原始的 x API 实现——例如在适当的时候作为后备。

编辑:

对于您特定的新添加的示例代码。

mock-x.c:

#include "x.h"
#include "mock-parameters.h"

Set_foo_params set_foo_params;

int mock_set_foo (X * x, int i) {
    // Stash parameter(s)
    set_foo_params.x = x;
    set_foo_params.i = i;

    return set_foo_params.result;
}

其余文件不变。 GCC 应该使用命令分别编译 object.cpp

g++ -Dset_foo=mock_set_foo -c object.cpp -o object.o
g++ test.cpp object.o x.c mock_x.c

GCC 选项-Dset_foo=mock_set_foo 的作用与

#define set_foo mock_set_foo

在命令行中每个源文件的开头这个选项之后。 此选项广泛用于编译时配置目的。

【讨论】:

  • x.cx.h 来自我无法控制的第三方库。我认为这个实现不会奏效,但感谢您的尝试。
  • @Zak 这一点变化不大。如果您无法控制 xc 那么您可以简单地编写类似 之类的内容(可能有条件) #define#include-ing 之前,您需要将所有名称模拟为您喜欢的任何替代名称。在 mock-x.c 中使用替代名称来定义替代实现。之后,您可以在 object.c#include 而不是 (同时仍然是 #include-ing in test.c)。你需要代码示例还是你自己想办法?
  • 我明白你的意思。但是,我无权采取如此严厉的做法。我正在处理的代码已经在生产中,所以我不能修改它或添加新文件。我的目标是利用链接器,这样我就可以进行这些测试。到时候我会重构代码库,让它测试友好,我就不用那么费劲去编译单元测试了。
  • @Zak 您对可能的解决方案范围设置了非常奇怪的限制。你所有的主张听起来自相矛盾。显然你不应该改变生产代码的行为(除非你打算这样做)。但是是什么阻止您更改单元测试的代码?你怎么可能在不添加新文件或新代码的情况下编写单元测试(或任何新内容)?您的单元测试不应该在生产环境中运行,对吗?或者,也许您可​​以使用一些链接技巧,为 test.cobject.c 创建单独的动态可加载二进制文件,但我建议不要这样做 - 不容易。
  • 澄清一下,我可以根据需要添加尽可能多的额外测试/模拟文件,并且我可以随意更改单元测试。在此示例中,我无法修改 x.hx.cobject.hppobject.cpp
猜你喜欢
  • 2014-09-14
  • 2015-01-16
  • 1970-01-01
  • 1970-01-01
  • 2016-10-31
  • 2023-03-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多