Какова роль утверждений в программах на C ++, имеющих модульные тесты?

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

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

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


person lhumongous    schedule 07.04.2010    source источник
comment
Обратите внимание, что в Windows Boost.Test позволяет перехватывать утверждения как исключения, поэтому запущенное утверждение обычно просто не проходит тест и может быть перехвачено, например, чтобы не провалить тест.   -  person Martin Ba    schedule 22.11.2011


Ответы (10)


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

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

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

person Orion Edwards    schedule 07.04.2010

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

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

Комментаторы отметили, что:

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

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

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

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

person Will    schedule 07.04.2010
comment
Я не голосовал против вас, но я думаю, что проблема в том, что действительный модульный тест может запускать assert. Рассмотрим модульный тест, который проверяет, правильно ли функция обрабатывает недопустимый ввод. Утверждение сработает, потому что ввод недопустим, но модульный тест очень подходит, поскольку он тестирует этот случай. - person chollida; 08.04.2010
comment
Гораздо более вероятно, что утверждение обнаружило ошибку в модульном тесте. Я не голосовал против тебя, кстати. - person ; 08.04.2010
comment
Не могли бы вы пояснить, почему, по вашему мнению, более вероятно, что модульный тест нарушает ограничения тестируемого кода? Если модульный тест просто вызывает функцию класса и запускает assert, разве это не означает, что в тестируемом коде может быть проблема? - person lhumongous; 08.04.2010
comment
@lhumongous Если вы тестируете функцию на то, как она обрабатывает «недопустимые параметры», вы не можете жаловаться на то, что программист вставил утверждения, чтобы уловить это. Утверждения используются, чтобы привлечь внимание к скомпрометированным ограничениям. С небольшим отклонением, я считаю, что модульные тесты для проверки недопустимых параметров не так полезны, как фаззеры и другие системные тесты. Скомпилируйте с помощью -Wall, просканируйте с помощью lint / coverity, запустите вашу программу в valgrind / purify, засорьте ее утверждениями и проведите бесконечный тест на обезьянах. Вот как выкуриваете насекомых. - person Will; 09.04.2010

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

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

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

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

person Josh Kelley    schedule 07.04.2010
comment
Если функция не принимает значения NULL, не следует ли вызывать ее со ссылкой вместо указателя? Если мы не говорим о C. - person Chris Bednarski; 08.04.2010
comment
@KNoodles: char * или const char *; соблюдение существующего стиля кодирования или стандартов кодирования; typedef void * РУЧКА ... - person Josh Kelley; 08.04.2010
comment
Утверждения предназначены для выявления внутренней несогласованности, а не недопустимого ввода. Исключения составляют исключительные случаи. Модульные тесты предназначены для проверки как правильной работы функции, так и правильной обработки ошибок. Модульные тесты, которые не оспаривают потенциально плохой ввод, не обеспечивают хорошего покрытия - person Stefan Sullivan; 15.12.2016

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

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

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

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

person Péter Török    schedule 07.04.2010

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

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

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

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

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

person R Samuel Klatchko    schedule 07.04.2010
comment
Если вы хотите, чтобы это было так (я не кстати), почему бы просто не определить ваше assert для выдачи std :: runtime_error (), если DEBUG не определен? - person Viktor Sehr; 08.04.2010
comment
@ViktorSehr - потому что поведение assert стандартизировано, и я думаю, что было бы сложно изменить поведение. - person R Samuel Klatchko; 08.04.2010
comment
В таком случае нельзя было бы использовать обертку? Или просто выдать ошибку времени выполнения вне зависимости от настроек сборки? - person UncleBens; 08.04.2010

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

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

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

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

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

person Steve Jessop    schedule 07.04.2010

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

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

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

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

person Jim Flood    schedule 07.04.2010

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

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

person Len Holgate    schedule 07.04.2010

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

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

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

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

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

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

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

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

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

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

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

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

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

person Sohail Si    schedule 07.06.2016