【问题标题】:How do I test a function whose output depends on another function?如何测试其输出依赖于另一个函数的函数?
【发布时间】:2011-02-05 12:33:18
【问题描述】:

我对测试很陌生,所以如果我在任何时候完全走错了方向,请告诉我。话虽如此,假设我想测试以下函数 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都是我自己写的函数,我已经测试过bar。

由于 foo 的输出是以 bar 的输出为条件的,我假设为了测试 foo,我需要创建一个 bar 的 mock。为了做到这一点,并假设 bar 的定义保存在与 foo 不同的源文件中,我可以创建一个新的源文件,包含它而不是找到 bar 的实际定义的源文件,然后放一个 mock该文件中的条形图。

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 而不更改我的源代码,这将是一个真正的痛苦。

【问题讨论】:

  • 可以在没有模拟 bar 函数的情况下对 foo 函数进行有效的单元测试吗?如果是这样,只需创建一个涵盖 foo 和 bar 组合功能的单元测试。

标签: c unit-testing


【解决方案1】:

嗯,有几种方法可以解决这个问题。

  • 当设置了UNITTEST 标志时,您可以使用预处理器挂钩来换出bar()

    #ifdef UNITTEST
    return mockBar();
    #else
    return bar();
    #endif
    
  • 您可以模拟依赖注入,并需要一个指向bar() 的指针作为函数的参数。我并不是说这在实践中是个好主意,但你可以做到。

    void foo( void (*bar)() ) {
    

我确定还有其他人,但我脑海中只有 2 个......

【讨论】:

  • 我认为预处理器钩子对我来说是最好的方法。
【解决方案2】:

您想要做的是用一个返回已知值的存根替换被调用的函数。使用外部依赖项(即数据库或网络代码)时也是如此。对于 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

【讨论】:

    【解决方案3】:

    有用于模拟的库。这些库通常会找到解决这些问题的方法。复杂的库将允许您在测试中配置 bar() 在测试中的每个点应返回的内容。

    我不确定他们是否会很好地处理bar()foo() 在同一个源文件中的情况,但他们可能会。在这种情况下,我会认为 bar()foo() 属于同一单元,但这是完全不同的论点。

    以下是来自 GoogleMock 的 C++ 代码片段 (source) 作为示例。它创建了一个模拟海龟对象,Painter 应该调用 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 的库。

    【讨论】:

    • Heya Pace,感谢您的回复,但我只是想强调一下,我认为 C 和 C++ 的测试非常不同,并且根据我的问题标签,我只使用 C。
    • 那肯定会改变一切。我从未见过一个普通的 C 模拟库,甚至不知道如何做到这一点。
    【解决方案4】:

    bar() 是一个尴尬的依赖吗?使用 bar 的实际实现对 foo 进行单元测试有问题吗?

    如果不是,那么我认为没有问题。您不必模拟所有内容。

    【讨论】:

      猜你喜欢
      • 2011-05-03
      • 1970-01-01
      • 2021-04-20
      • 2022-11-16
      • 1970-01-01
      • 1970-01-01
      • 2019-12-04
      • 2017-03-27
      • 2021-04-06
      相关资源
      最近更新 更多