Какова роль утверждений в программах на C ++, в которых есть модульные тесты? - PullRequest
14 голосов
/ 08 апреля 2010

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

Я мог бы легко обойти это, отключив утверждения, когда я выполняю модульное тестирование.Но я начинаю задаваться вопросом, должны ли модульные тесты смягчать необходимость утверждений времени выполнения.Это правильная оценка?Должны ли модульные тесты заменять утверждения времени выполнения, поскольку они происходят раньше в конвейере (т. Е. Ошибка обнаруживается в неудачном тесте, а не во время работы программы).

С другой стороны, мне не нравитсядобавление программных сбоев в код (например, if (param == NULL) return false;).Утверждение времени выполнения, по крайней мере, облегчает отладку проблемы в случае, если в модульном тесте пропущена ошибка.

Ответы [ 10 ]

17 голосов
/ 08 апреля 2010

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

Это довольно фундаментальный момент. Модульные тесты не предназначены для замены утверждений (которые ИМХО являются стандартной частью для создания кода хорошего качества), они должны дополнять их.

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

9 голосов
/ 08 апреля 2010

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

Но гораздо более вероятно, что ваш код модульного теста нарушает ограничения модулей, которые он тестирует - код вашего модульного теста содержит ошибки!

Комментаторы подняли вопрос о том, что:

Рассмотрим модульный тест, который подтверждает, что функция правильно обрабатывает неверный ввод.

Утверждение - это способ программиста обрабатывать некорректный ввод, прерывая программу. При отмене программа работает нормально.

Утверждения присутствуют только в отладочных сборках (они не компилируются, если определен макрос NDEBUG), и важно проверить, что программа делает что-то разумное в сборках выпуска. Попробуйте запустить недопустимый модульный тест параметров в сборках выпуска.

Если вы хотите, чтобы оба мира - проверка срабатываний утверждений в отладочных сборках - то вы хотите, чтобы ваш тестовый модуль в потоке захватывал эти утверждения. Вы можете сделать это, указав свой собственный assert.h вместо системного; макрос (вы бы хотели, чтобы __LINE__ и __FILE__ и ## expr) вызывал написанную вами функцию, которая может генерировать пользовательский AssertionFailed при запуске в модульном тесте. Это, очевидно, не захватывает утверждения, скомпилированные в другие двоичные файлы, с которыми вы связываетесь, но не компилируется из исходного кода с помощью пользовательского утверждателя. (Я бы рекомендовал это вместо предоставления своего abort(), но это еще один подход, который вы могли бы рассмотреть для достижения той же цели.)

4 голосов
/ 08 апреля 2010

Утверждения ловятся, когда ваш код используется некорректно (нарушая его ограничения или предварительные условия - используя библиотеку без инициализации, передавая NULL функции, которая не принимает ее и т. Д.).

Модульные тесты подтверждают, что ваш код работает правильно, если он используется правильно.

Утверждения также отлавливаются, когда ваш код вошел в состояние, которое вы считали невозможным (которое, поскольку считается невозможным, не может быть проверено модулем).

Интересно, что по крайней мере одна среда модульного тестирования C ++ ( Google Test ) поддерживает тесты на смерть , которые являются модульными тестами, которые проверяют правильность ваших утверждений (чтобы вы могли знать свои утверждения делают свою работу по обнаружению недопустимых состояний программы).

3 голосов
/ 08 апреля 2010

Я использую утверждения только для проверки того, что "никогда не произойдет". Если срабатывает assert, то где-то была допущена программная ошибка.

Предположим, что метод берет имя входного файла, а модульный тест передает ему имя несуществующего файла, чтобы увидеть, было ли выбрано исключение "файл не найден". Это не то, что "никогда не случится". возможно не найти файл во время выполнения. Я бы не использовал assert в методе, чтобы убедиться, что файл найден.

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

В случае вашего вопроса, если функция утверждает! = NULL, либо модульный тест является неправильным и не должен отправлять в NULL, потому что это никогда не произойдет, либо модульный тест действителен, и NULL может быть отправлен, и функция неверна и не должна утверждать! = NULL и должна вместо этого обработать это условие.

3 голосов
/ 08 апреля 2010

Две возможности здесь:

1) Поведение функции определяется (явно ее собственным интерфейсом или общими правилами для проекта), когда входное значение равно нулю. Поэтому модульный тест должен проверить это поведение. Таким образом, вам нужен обработчик для запуска процесса, запускающего тестовый пример, и обработчик проверяет, что код сработал для подтверждения и прерван, или , вам нужно каким-то образом смоделировать assert.

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

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

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

3 голосов
/ 08 апреля 2010

Обычно вы не должны быть в состоянии отключить утверждения, так как они должны ловить «невозможные ситуации». Если срабатывает assert, это должно указывать на ошибку.

Единственным исключением из этого правила является то, что многие разработчики используют утверждения для проверки правильности аргументов. Это нормально, если есть резервная копия для сборок без утверждений:

assert(arg != 0);
if (arg != 0)
    throw std::runtime_error();

Таким образом, если неверный аргумент возникает только при определенных условиях (т. Е. В поле), он все равно будет перехвачен.

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

3 голосов
/ 08 апреля 2010

IMO утверждает и модульные тесты являются довольно независимыми понятиями.Ни один из них не может заменить другой.

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

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

Нельзя при модульном тестировании гарантировать, что его среда всегда будет выполнять свою часть контрактав реальных обстоятельствах.Более того, в упомянутую среду входят будущие разработчики, которые могут не иметь ни малейшего представления о контрактном интерфейсе, регулирующем использование этого класса (будь то скрытый или тщательно задокументированный).А также множество других программных и аппаратных компонентов, которые могут измениться и / или сломаться в любое время, что не поддается контролю со стороны разработчиков этой конкретной программы.

1 голос
/ 08 апреля 2010

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

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

0 голосов
/ 07 июня 2016

1- Утверждение - хороший способ прояснить инварианты (инварианты цикла) во время разработки алгоритмов. Это может быть полезно для «читабельности», а также для отладки. В языке Eiffel Бертрана Мейера есть ключевое слово , инвариант . На других языках assert может использоваться для этой цели.

2- Assert также может использоваться в других случаях в качестве промежуточного решения при разработке и постепенно удаляться по мере завершения кода. То есть в качестве предметов TODO. Некоторые из assert (те, которые не указаны в пункте 1 выше) необходимо заменить обработкой исключений и т. Д. Их гораздо легче обнаружить, если все такие проверки показаны как утверждения.

3- Я иногда использую его для уточнения типа входных данных в языках, в которых нет системы проверки типов (Python и JavaScript), в определенных контекстах (например, при разработке новых алгоритмов). Не уверен, что это рекомендуемая практика. Как и пункт № 1, речь идет об увеличении читабельности программы.

0 голосов
/ 22 ноября 2011

Сначала , чтобы модульный тест набрал assert (или ASSERT или _ASSERT или _ASSERTE в сборках Windows), модульному тесту потребуется запустить код подтест с отладочной сборкой.

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

Секунда , можно использовать нормативный подход с утверждениями -

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

В этом случае нетмодульный тест должен выдвинуть утверждение, потому что вызов кода таким способом, которым выдано утверждение, не должен быть возможным.

или можно использовать «прагматический» подход с утверждениями:

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

Если вы выберете прагматичный подход, то применение модульного теста к утверждению означает модульное тестирование.вызывается код, который не вполне поддерживается кодом.Это может означать, что код «ничего не делает» в сборке релиза, или это может означать, что код падает в сборке релиза, или это может означать, что код делает «что-то интересное».

Вот варианты, которыеМне известно, что я использую:

  • Если утверждение сопровождается дополнительной проверкой, чтобы сделать вызов "безвредным", сделайте модульный тест тест для утверждения (в отладке) и для «безвредного» условия в выпуске.
  • Для сбоев или «чего-то интересного», либо нет никакого модульного теста, который имеет смысл, или вы можете сделать «только для отладки» модульный тест, который проверяет, что вы действительнополучить утверждение (хотя я не уверен, что это полезно).
...