我正在为一个依赖于第三方 C 和 C++ 库以及第一方 C 库的 C++ 类编写单元测试。我遇到了麻烦,因为我希望被测类使用模拟的第三方 C 库,而测试运行器使用真正的第 3 方 C 库。
- 我正在编写单元测试,被测试的类依赖于
libx
。 - 我创建了一个
libmockx
,它允许我测试参数并注入返回值。 - 被测试的类需要链接到
libmockx
以便我可以检查和控制它的行为。 - 单元测试应用程序需要链接到
libx
才能制定/解析libx
数据类型。
使用哪种模式或方法将测试运行程序链接到libx
,将被测类链接到libmockx
,然后将测试运行程序链接到被测试的类?许多解决方案都讨论让链接器做"肮脏的工作",但ld
有 100 多个参数,我不知道如何让它工作。
目前,我对所有模拟实现都有重新定义错误,我需要一种方法来解决它(无论是否涉及链接器)。
编辑如下: (回应评论)
想象一下 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.cpp
与x.c
链接,同时仍然将object.cpp
与mock_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;
}
既然你澄清了你的object.c和test.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;
}
}
模拟-x.cpp:
namespace mock_x {
int foo()
{
int res = -1;
// mock implementation
return res;
}
}
对象.cpp:
#define X_DEBUG
#include <x.h>
int obj_bar()
{
int res = X::foo(); // uses mock implementation
return res;
}
}
测试.cpp:
#include <x.h>
int test_bar()
{
int res = X::foo(); // uses real implementation
return res;
}
}
显然,这种方法是可扩展的 - 你可以在这个命名空间X中放置任意数量的类/函数/对象。通用功能可以在标头
通常:
我肯定会朝着创建 mock-x 实现的方向看,作为一组不同的链接器符号。这绝对是更可控和灵活的方法。您甚至可以使用来自 mock-x 功能的原始 x API 实现 - 例如,在适当的时候作为回退。
编辑:
对于您新添加的特定示例代码。
模拟-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 应该使用命令单独编译对象.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
在此选项之后的命令行中每个源文件的开头。 此选项广泛用于编译时配置目的。