Комментарии 10
Со сложением пришла в голову такая мысль, что, на самом деле, операция сложения имеет вполне точное математическое определение: Отображение Z x Z -> Z которое симметрично, ассоциативно, и имеет "0" в качестве "нейтрального элемента" (∀ x ∈ Z: x + 0 = x), т.ч. м.б. в тестах именно это сначала и надо тестировать :))
Я бы ещё посоветовал почитать книгу "Принципы юнит-тестирования" (Владимир Хориков). Там все эти вопросы объясняются развёрнуто.
ни отрицательные числа, ни большие числа, сумма которых выходит за пределы размера 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" :))
написание тестов после написания кода тоже хорошая практика
Я пришел к смешанному подходу. Тесты на контракты (например на проверку входных данных) обычно пишу сразу же, остальное уже по ходу реализации.
Юнит-тесты покрывают исключительно бизнес-логику. Логики всегда мало, она всегда синхрона и написана в процедурном стиле стиле. Примеры с калькулятором - неправильны.
Чтобы покрыть логику, ее сначала нужно отделить от кода, и отделить данные. Без ioc или ServiceLocator не имеет смысла заниматься тестами.
Топ 10 самых распространенных ошибок в использовании юнит-тестов