Контрольные примеры VS ASSERTION statement

В моем большинстве проектов на С++ я сильно использовал оператор ASSERTION следующим образом:

int doWonderfulThings(const int* fantasticData)
{
    ASSERT(fantasticData);
    if(!fantasticData)
        return -1;
    // ,,,
    return WOW_VALUE;
}

Но сообщество TDD, похоже, любит делать что-то вроде этого:

int doMoreWonderfulThings(const int* fantasticData)
{
    if(!fantasticData)
        return ERROR_VALUE;
    // ...
    return AHA_VALUE;
}

TEST(TDD_Enjoy)
{
    ASSERT_EQ(ERROR_VALUE, doMoreWonderfulThings(0L));
    ASSERT_EQ(AHA_VALUE, doMoreWonderfulThings("Foo"));
}

Просто с моим опытом первые подходы позволяют мне удалить так много тонких ошибок. Но подходы TDD - очень умная идея для обработки устаревших кодов.

"Google" - они сравнивают "ПЕРВЫЙ МЕТОД" с "Прогуливаться по берегу с живым жилетом, плавать в океане без какой-либо безопасной охраны".

Какой из них лучше? Какой из них делает программное обеспечение надежным?

Ответ 1

В моем (ограниченном) опыте первый вариант довольно безопасен. В тестовом случае вы проверяете только предопределенный ввод и сравниваете результат, это хорошо работает, пока проверяется все возможные краевые условия. Первый вариант просто проверяет каждый вход и, таким образом, проверяет "живые" значения, он быстро отфильтровывает ошибки, однако он имеет ограничение производительности.

В Code Complete Стив Макконнелл узнает, что первый метод можно успешно использовать для фильтрации ошибок в отладке построить. В сборке вы можете отфильтровывать все утверждения (например, с флагом компилятора), чтобы получить дополнительную производительность.

По-моему, лучший способ - использовать оба метода:

Метод 1 для улавливания незаконных значений

int doWonderfulThings(const int* fantasticData)
{
    ASSERT(fantasticData);
    ASSERTNOTEQUAL(0, fantasticData)

    return WOW_VALUE / fantasticData;
}

и метод 2 для проверки крайних случаев алгоритма.

int doMoreWonderfulThings(const int fantasticNumber)
{
    int count = 100;
    for(int i = 0; i < fantasticNumber; ++i) {
        count += 10 * fantasticNumber;
    }
    return count;
}

TEST(TDD_Enjoy)
{
    // Test lower edge
    ASSERT_EQ(0, doMoreWonderfulThings(-1));
    ASSERT_EQ(0, doMoreWonderfulThings(0));
    ASSERT_EQ(110, doMoreWonderfulThings(1));

    //Test some random values
    ASSERT_EQ(350, doMoreWonderfulThings(5));
    ASSERT_EQ(2350, doMoreWonderfulThings(15));
    ASSERT_EQ(225100, doMoreWonderfulThings(150));
}

Ответ 2

Оба механизма имеют ценность. Любая приемлемая тестовая среда поймет стандарт assert() в любом случае, поэтому тестовый прогон, который вызывает отказ assert, приведет к неудачному тесту.

Обычно у меня есть ряд утверждений в начале каждого метода С++ с комментарием '//preconditions'; это просто проверка работоспособности состояния, которое я ожидаю, когда объект будет вызван. Эти лакокрасочные покрытия прекрасно встраиваются в любую инфраструктуру TDD, потому что они не только работают во время выполнения, когда вы тестируете функциональность, но также работают во время тестирования.

Ответ 3

Нет причин, по которым ваш тестовый пакет не может заражать утверждения, например, в doMoreWonderfulThings. Это можно сделать либо путем поддержки обработчика ASSERT механизма обратного вызова, либо в ваших тестах содержится блок try/catch.

Ответ 4

Я не знаю, к какой части субсети TDD вы обращаетесь, но шаблоны TDD, с которыми я столкнулся, либо используют Assert.AreEqual() для положительных результатов, либо иным образом используют механизм ExpectedException (например, атрибуты в .NET) чтобы объявить ошибку, которая должна соблюдаться.

Ответ 5

В С++ я предпочитаю метод 2 при использовании большинства тестовых фреймворков. Обычно это упрощает понимание отчетов об ошибках. Это бесценно, когда тест прошел через несколько лет после того, как тест был написан.

Моя причина в том, что большинство баз данных тестирования С++ распечатывают файл и номер строки, где было указано утверждение, без какой-либо информации о трассировке стека. Поэтому большую часть времени вы получите номер строки отчетности внутри функции или метода, а не внутри тестового примера.

Даже если утверждение уловлено и повторно подтверждено от вызывающей стороны, строка отчетности будет с оператором catch и не может находиться где-нибудь близко к строке тестового случая, которая вызвала метод или функцию, которые утверждались. Это может быть очень неприятно, если функция, которая утверждала, может быть использована несколько раз в тестовом случае.

Однако есть исключения. Например, в базе данных Google есть сводный оператор трассировки, который будет печатать как часть трассы, если возникает исключение. Таким образом, вы можете обернуть вызов обобщенной тестовой функции с помощью области трассировки и легко указать, в строке или двух, какая строка в точной тестовой версии завершилась неудачно.