Как стать автором
Обновить

Комментарии 10

Со сложением пришла в голову такая мысль, что, на самом деле, операция сложения имеет вполне точное математическое определение: Отображение Z x Z -> Z которое симметрично, ассоциативно, и имеет "0" в качестве "нейтрального элемента" (∀ x ∈ Z: x + 0 = x), т.ч. м.б. в тестах именно это сначала и надо тестировать :))

Но представления чисел в программе - это не совсем то, что подразумевается в школьной арифметике. int - как правило, кольца, float - непоймичто ;).

Я бы ещё посоветовал почитать книгу "Принципы юнит-тестирования" (Владимир Хориков). Там все эти вопросы объясняются развёрнуто.

ни отрицательные числа, ни большие числа, сумма которых выходит за пределы размера int, ни сложение с нулем здесь не проверены.

О, да... Кусочек из старого моего:

Hidden text
#include <stdio.h>
#include <errno.h>
#include <inttypes.h>

static int test( const char *in, int base, intmax_t want )
{
    intmax_t rc = strtoimax( in, NULL, base );

    if( rc != want ) {
        fprintf( stderr, "Error in \"%s\": expect %lld, got %lld.\n", in, want, rc );
        return 1;
    }
    return 0;
}

int main()
{
    int errors = 0;

    errors += test( " -123junk", 10, -123 ); /* explicit base 10           */
    errors += test( "11111111", 2, 255 );    /* explicit base 2            */
    errors += test( "XyZ", 36, 44027 );      /* explicit base 36           */
    errors += test( "010", 0, 8 );           /* octal auto-detection       */
    errors += test( "10", 0, 10 );           /* decimal auto-detection     */
    errors += test( "0x10", 0, 16 );         /* hexadecimal auto-detection */

    /* overflow, must set errno */
    errno = 0;
    strtoimax( "9223372036854775808", NULL, 10 );
    if( errno != ERANGE ) {
        fprintf( stderr, "Overflow test failed.\n" );
        ++errors;
    }

    /* invalid base, must return 0 */
    if( strtoimax( "10", NULL, 44 ) != 0 ) {
        fprintf( stderr, "Invalid base test failed.\n" );
        ++errors;
    }

    /* base and input mismatch, must return 0 */
    if( strtoimax( "333", NULL, 2 ) != 0 ) {
        fprintf( stderr, "Base and input mismatch test failed.\n" );
        ++errors;
    }

    if( errors ) {
        fprintf( stderr, "%d tests failed!\n", errors );
    }
    else {
        puts( "All tests passed correctly." );
    }
    return errors;
}

Однако, ни отрицательные числа, ни большие числа, сумма которых выходит за пределы размера int, ни сложение с нулем здесь не проверены.

Отрицательные и сложение с 0 и нет необходимости проверять. Можно точно так же еще написать, что не проверено сложение с 5. Или 100. Или 99. Или двух одинаковых чисел.

Очередная статья в духе «Нужно делать так, как нужно. А как не нужно, делать не нужно!»

По пунктам «Недостаточное покрытие» и «Переизбыток тестов» – уже набил оскомину этот пример с калькулятором. Для тестирования 1 строчки столько усилий. Как мне кажется, если вы не разрабатываете для космического агентства, где цена ошибки - миллиарды, то такие примитивные вещи вообще не нужно тестировать. Несколько хороших программистов и налаженная процедура приемки пул-реквестов значительно ускорят разработку. Иначе до релиза вы просто не дойдете.

Использовать генератор данных для тестов – то же опасная штука, т.к. если тесты начнут падать случайным образом, то будет больно.

«Нетестируемый код» - пример «хорошей» реализации от автора подразумевает, что у нас есть IoC-контейнер, что не всегда так. Ответственность за получение экземпляра  логгера передается на более высокий уровень, что повлечет за собой дополнительный рефакторинг кода. В принципе, в данном примере достаточно было бы сделать 2 конструктора – 1) без параметров, который создаем экземпляр ILogger по умолчанию; 2) для тестов, который принимает мок экземпляра ILogger.

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

Как мне кажется, если вы не разрабатываете для космического агентства, где цена ошибки - миллиарды, то такие примитивные вещи вообще не нужно тестировать.

Если непрерывно (например, в CI) ведется анализ покрытия, то нужно. Потому что если у меня при каждом билде покрытие, например, от 70 до 80%, то мне каждый раз надо разбираться сознательно это не покрыто или я в тестах которые только что написал упустил какие-то кейсы. Это тоже самое, что с процентом прохождения тестов - он либо 100, либо если он не 100, то он 0 и разницы 60 это или 90 вообще никакой. Исключение - это код который юнит-тестированию не поддаётся чисто технически, например, вызов какого-то стороннего API, результат которого ты в контексте тестов никак не можешь контролировать. Тогда такой кусок кода просто надо исключать из анализа покрытия соответствующими средствами используемого фреймворка.

В принципе, в данном примере достаточно было бы сделать 2 конструктора – 1) без параметров, который создаем экземпляр ILogger по умолчанию; 2) для тестов, который принимает мок экземпляра ILogger.

+1 Это стандартный прием. Я в своей команде когда-то даже название для него придумал: "Poor man DI" :))

написание тестов после написания кода тоже хорошая практика

Я пришел к смешанному подходу. Тесты на контракты (например на проверку входных данных) обычно пишу сразу же, остальное уже по ходу реализации.

 Я в своей команде когда-то даже название для него придумал: "Poor man DI" :))

Это не ты, это Mark Seemann придумал.

Ну, супер, значит я в хорошей компании :))

Юнит-тесты покрывают исключительно бизнес-логику. Логики всегда мало, она всегда синхрона и написана в процедурном стиле стиле. Примеры с калькулятором - неправильны.

Чтобы покрыть логику, ее сначала нужно отделить от кода, и отделить данные. Без ioc или ServiceLocator не имеет смысла заниматься тестами.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий