如何测试其输出取决于另一个函数的函数?

时间:2011-02-05 12:33:19

标签: c unit-testing

我对测试非常陌生,所以如果我在任何时候完全走错方向,请告诉我。话虽如此,假设我想测试以下函数,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。这将是一个真正的痛苦。

4 个答案:

答案 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的单元测试是否有问题?

如果没有,那我就没有问题。你不必嘲笑一切。