开发者

How do I test a function whose output depends on another function?

I'm very new at testing so please let me know if I am just going off in completely the wrong direction at any point. Having said that, assume I want to test the following function, 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;
}

In this example, both foo and bar are functions written by myself and I have already tested bar.

Since the output of foo is conditional on the output of bar, I assume that in order to test foo, I need to create a mock of bar. In order to do that, and assuming that the definition of bar is kept inside a separate source file from foo, I could create a new source file, include that instead of the one where the actual definition of bar is found, and put a mock of bar in that file.

int bar(void) {
  return HARD_CODED_VALUE;
}

However, there are 2 problems with this approach:

1) What happens if bar returns multiple values (such as an error code or an actual value) and I need to ensure that foo reacts correctly for each possibility? I can't create multiple definitions for bar. One thought I 开发者_Python百科did have was to create a static int in bar and then increment it every time bar gets called. Then I just have a conditional on this int, call bar multiple times and thus return multiple values. However, I am unsure whether introducing more complex logic into a mock function is good practice or if there is a better way to achieve this:

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) What happens if bar is in the same source file as foo? I can't redefine bar nor separate foo and bar without altering my source code which would be a real pain.


Well, there are a few ways around that problem.

  • You could use preprocessor hooks to swap out bar() when a UNITTEST flag is set:

    #ifdef UNITTEST
    return mockBar();
    #else
    return bar();
    #endif
    
  • You could simulate Dependency Injection, and require a pointer to bar() as a parameter to the function. I'm not saying that's a great idea in practice, but you could do it.

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

I'm sure there are others, but that's just 2 that came off the top of my head...


What you want to do is substitute the called function with a stub returning known values. The same would apply when using an external dependency, i.e. a database or networking code. With C there are two usable "seams" (to use the terminology from Working Effectively with Legacy Code) that allow you to perform that substitute:

  • Using preprocessor commands to replace the function body with a macro, e.g.

    #ifdef TEST
    #define bar(x)  { if (x) then y; else z; }
    #endif
    
  • Move bar(x) into a separate library, and then maintain two versions of the library. The first is your production code and the second is a test library that contains a test stub of bar(x).

A third option is to use dependency injection, by refactoring the bar(x) call out into a function pointer parameter as ircmaxell demonstrated.

void foo( void (*bar)() )

I have tried these approaches with non-OO C++ code and found the first to be by far the most useful. The second introduces a pretty tough maintainability issue (multiple versions of the same libraries and the functions within need to be maintained in conjunction), while the latter obviously negatively impacts upon the readability and understandability of the code.

The preprocessor directives, on the other hand, can be quite localized and the alternate definitions can be separated out into a header file that is only included if tested, i.e.

#ifdef TEST
    #include "subsystem_unittest.h"
#endif


There are libraries for mocking. These libraries usually find a way to address those very questions. Sophisticated libraries will allow you to configure in your test what bar() should return at each point in the test.

I'm not sure they'll be handling the case where bar() and foo() are in the same source file very well but they might. In this case I would consider bar() and foo() to be part of the same unit but that is an entirely different argument.

Here is a C++ code fragment (source) from GoogleMock as an example. It creates a Mock turtle object which the Painter should call the PenDown method once and when it does the PenDown method will return 500. If the Painter doesn't call PenDown then the test would fail.

#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();
}

Of course this particular library is using OOP which you may or may not be doing. I would guess there are other libraries out there for non-OOP too.


Is bar() an awkward dependency ? Is there a problem with the unit test for foo using the actual implementation of bar ?

If not, then I don't see a problem. You do not have to mock everything.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜