Как проверить функцию, вывод которой зависит от другой функции? - PullRequest
3 голосов
/ 05 февраля 2011

Я очень новичок в тестировании, поэтому, пожалуйста, дайте мне знать, если я в какой-то момент просто ухожу в совершенно неправильном направлении. Сказав это, предположим, что я хочу проверить следующую функцию, 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. Чтобы сделать это, и предположив, что определение bar хранится в отдельном исходном файле от foo, я мог бы создать новый исходный файл, включить его вместо того, где находится фактическое определение bar, и поместить макет бара в этом файле.

int bar(void) {
  return HARD_CODED_VALUE;
}

Однако у этого подхода есть 2 проблемы:

1) Что произойдет, если bar возвращает несколько значений (например, код ошибки или фактическое значение), и мне нужно убедиться, что foo правильно реагирует на каждую возможность? Я не могу создать несколько определений для бара. Одна мысль, которая у меня была, заключалась в том, чтобы создать статический int в bar, а затем увеличивать его каждый раз, когда вызывается 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? Я не могу переопределить ни бар, ни разделить foo и bar, не изменив мой исходный код, что было бы настоящей болью.

Ответы [ 4 ]

3 голосов
/ 05 февраля 2011

Что ж, есть несколько способов обойти эту проблему.

  • Вы можете использовать ловушки препроцессора для замены bar(), когда установлен флаг UNITTEST:

    #ifdef UNITTEST
    return mockBar();
    #else
    return bar();
    #endif
    
  • Вы можете смоделировать внедрение зависимостей и потребовать указатель на bar() в качестве параметра функции.Я не говорю, что это отличная идея на практике, но вы могли бы сделать это.

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

Я уверен, что есть и другие, но это всего лишь 2, которые пришли сверхумоей головы ...

1 голос
/ 06 февраля 2011

Что вы хотите сделать, это заменить вызываемую функцию заглушкой, возвращающей известные значения.То же самое применимо при использовании внешней зависимости, то есть базы данных или сетевого кода.С C есть два «шва», которые можно использовать (чтобы использовать терминологию из «Эффективной работы с устаревшим кодом»), которые позволяют выполнить эту замену:

  • Использование команд препроцессора для замены тела функции намакрос, например

    #ifdef TEST
    #define bar(x)  { if (x) then y; else z; }
    #endif
    
  • Переместите панель (x) в отдельную библиотеку, а затем сохраните две версии библиотеки.Первый - ваш рабочий код, а второй - библиотека тестов, содержащая тестовую заглушку bar (x).

Третий вариант - использовать внедрение зависимостей путем рефакторинга bar (x) вызвать параметр указателя на функцию, как продемонстрировал ircmaxell.

void foo( void (*bar)() )

Я попробовал эти подходы с не-OO-кодом C ++ и нашел первый из них наиболее полезным.Второй вводит довольно сложную проблему удобства сопровождения (несколько версий одних и тех же библиотек и функций должны поддерживаться в связке), в то время как последняя явно отрицательно влияет на читабельность и понятность кода.

ПрепроцессорДирективы, с другой стороны, могут быть довольно локализованы, и альтернативные определения могут быть выделены в заголовочный файл, который включается только при проверке, то есть

#ifdef TEST
    #include "subsystem_unittest.h"
#endif
0 голосов
/ 07 февраля 2011

Является ли bar () неловкой зависимостью? Есть ли проблема с модульным тестом для foo, использующим фактическую реализацию bar?

Если нет, то я не вижу проблемы. Вам не нужно все издеваться.

0 голосов
/ 05 февраля 2011

Есть библиотеки для издевательств. Эти библиотеки обычно находят способ ответить на эти самые вопросы. Сложные библиотеки позволят вам настроить в тесте то, что bar() должно возвращать в каждой точке теста.

Я не уверен, что они будут обрабатывать случай, когда bar() и foo() находятся в одном и том же исходном файле очень хорошо, но они могут. В этом случае я бы считал bar() и foo() частью одной и той же единицы, но это совершенно другой аргумент.

Вот фрагмент кода C ++ ( source ) из GoogleMock в качестве примера. Он создает объект Mock turtle, который 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();
}

Конечно, эта конкретная библиотека использует ООП, что вы можете или не можете делать. Я предполагаю, что есть и другие библиотеки для не-ООП.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...