我对测试非常陌生,所以如果我在任何时候完全走错方向,请告诉我。话虽如此,假设我想测试以下函数,foo。
int foo(int i) {
//Lots of code here
i = bar();
//Some more changes to i happen here, conditional on what bar returned
return i;
}
在这个例子中,foo和bar都是我自己编写的函数,我已经测试了吧。
由于foo的输出是以bar的输出为条件的,我假设为了测试foo,我需要创建一个bar的模拟。为了做到这一点,并假设bar的定义保存在foo的单独源文件中,我可以创建一个新的源文件,包括而不是找到bar的实际定义的那个,并放一个模拟该文件中的栏。
int bar(void) {
return HARD_CODED_VALUE;
}
然而,这种方法存在两个问题:
1)如果bar返回多个值(例如错误代码或实际值)会发生什么,我需要确保foo对每种可能性做出正确反应?我不能为bar创建多个定义。我想到的一个想法是在bar中创建一个静态int,然后在每次调用bar时递增它。然后我只是对这个int有条件,多次调用bar,从而返回多个值。但是,我不确定是否将更复杂的逻辑引入模拟函数是一种好的做法,或者是否有更好的方法来实现这一点:
int bar(void) {
static int i = 0;
i++;
if(i == 1) {
return HARD_CODED_VALUE_1
}
else if(i == 2) {
return HARD_CODED_VALUE_2
}
else {
fprintf(stderr, "You called bar too many times\n");
exit(1);
}
}
2)如果bar与foo位于同一源文件中会发生什么?我不能重新定义bar,也不能在不改变我的源代码的情况下分离foo和bar。这将是一个真正的痛苦。
答案 0 :(得分:3)
嗯,有几种方法可以解决这个问题。
当设置bar()
标志时,您可以使用预处理程序挂钩交换UNITTEST
:
#ifdef UNITTEST
return mockBar();
#else
return bar();
#endif
您可以模拟依赖注入,并需要指向bar()
的指针作为函数的参数。我不是说这在实践中是一个好主意,但你可以做到。
void foo( void (*bar)() ) {
我确定还有其他人,但这只是我头脑中的2个......
答案 1 :(得分:1)
您要做的是将被调用的函数替换为返回已知值的存根。使用外部依赖项(即数据库或网络代码)时也是如此。有了C,有两个可用的"接缝" (使用有效使用遗留代码的术语),允许您执行替代:
使用预处理器命令将函数体替换为宏,例如
#ifdef TEST
#define bar(x) { if (x) then y; else z; }
#endif
将bar(x)移动到单独的库中,然后维护该库的两个版本。第一个是您的生产代码,第二个是包含bar(x)测试存根的测试库。
第三个选项是使用依赖注入,通过重构bar(x)调用函数指针参数,如ircmaxell所示。
void foo( void (*bar)() )
我尝试过使用非OO C ++代码的这些方法,并且发现了第一个迄今为止最有用的方法。第二个引入了一个相当棘手的可维护性问题(相同库的多个版本以及需要同时维护的函数),而后者显然会对代码的可读性和可理解性产生负面影响。
另一方面,预处理程序指令可以非常本地化,并且可以将备用定义分离为仅在测试时包含的头文件,即#ifdef TEST
#include "subsystem_unittest.h"
#endif
答案 2 :(得分:0)
有用于嘲笑的库。这些库通常会找到解决这些问题的方法。先进的库允许您在测试中配置bar()
在测试中每个点应返回的内容。
我不确定他们会处理bar()
和foo()
在同一源文件中的情况,但他们可能会这样做。在这种情况下,我会认为bar()
和foo()
是同一单位的一部分,但这是一个完全不同的论点。
以下是来自GoogleMock的C ++代码片段(source)。它创建了一个Mock龟对象,Painter应该调用PenDown方法一次,当PenDown方法执行PenDown方法时,它将返回500.如果Painter没有调用PenDown,则测试将失败。
#include "path/to/mock-turtle.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
using ::testing::AtLeast; // #1
TEST(PainterTest, CanDrawSomething) {
MockTurtle turtle; // #2
EXPECT_CALL(turtle, PenDown()) // #3
.WillOnce(Return(500));
Painter painter(&turtle); // #4
EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
} // #5
int main(int argc, char** argv) {
// The following line must be executed to initialize Google Mock
// (and Google Test) before running the tests.
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}
当然这个特定的库正在使用你可能会或可能不会做的OOP。我猜也有非OOP的其他库。
答案 3 :(得分:0)
bar()是一个尴尬的依赖吗?使用bar的实际实现,foo的单元测试是否有问题?
如果没有,那我就没有问题。你不必嘲笑一切。