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

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

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

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

Ответ 1

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

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

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

Ответ 2

Если ваш код unit test верен, то утверждение является ошибкой, которую обнаружил unit test.

Но гораздо более вероятно, что ваш код unit test нарушает ограничения тестируемых единиц - ваш код unit test глючит!

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

Рассмотрим unit test, который проверяет правильность правильной работы с неправильным входом.

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

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

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

Ответ 3

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

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

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

Интересно, что хотя бы одна модульная платформа тестирования С++ (Google Test) поддерживает

Ответ 4

ИМО утверждает, что модульные тесты являются довольно независимыми понятиями. Ни один из них не может заменить другого.

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

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

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

Ответ 5

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

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

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

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

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

Ответ 6

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

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

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

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

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

Ответ 7

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

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

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

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

Ответ 8

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

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

Ответ 9

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

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

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

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

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

или можно принять "прагматичный" подход с утверждениями:

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

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

Здесь параметры, которые мне известны:

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

Ответ 10

1- Assert - хороший способ прояснить инварианты (инварианты цикла) при разработке алгоритмов. Это может быть полезно для "удобочитаемости", а также для отладки. Язык Eiffel by Bertrand Meyer имеет ключевое слово инвариант. На других языках утверждение может использоваться для этой цели.

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

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