Использование assert
Другим относительно новым дополнением к Java является ключевое слово assert. Оно используется во время разработки программ для создания так называемых утверждений (assertion), представляющих собой условия, которые должны быть истинными во время выполнения программы. Например, вы можете иметь метод, который всегда возвращает положительное целое значение. Вы можете тестировать его утверждением, что возвращаемое значение больше нуля, используя оператор assert. Во время выполнения, если условие действительно истинно, то никаких других действий не выполняется. Однако если условие окажется ложным, будет возбуждено исключение AssertionError. Утверждения часто применяются при тестировании для верификации того, что некоторое ожидаемое условие действительно выполняется. В коде окончательной версии они, как правило, не. присутствуют.
Ключевое слово assert имеет две формы. Первая выглядит так:
Здесь condition — выражение, которое должно при вычислении дать булевский результат. Если результат равен true, то утверждение истинно, и никаких действий не выполняется. Если же условие дает false, значит, произошел сбой и возбуждается объект исключения по умолчанию AssertionError.
Вторая форма assert выглядит следующим образом:
В этой версии expr — значение, которое передается конструктору AssertionError. Это значение преобразуется в строковую форму и отображается, если утверждение ложно. Обычно вы специфицируете строку для ехрг, но разрешено любое выражение, отличное от void, до тех пор, пока оно допускает осмысленное строковое преобразование.
Ниже показан пример использования assert. В нем осуществляется проверка того, что возвращаемое значение getnum () положительно.
// Демонстрация assert.
class AssertDemo static int val = 3; // Возвращает целое.
static int getnum() return val—;
>
public static void main(String args[]) int n;
for(int i=0; i 0; // произойдет сбой, если n == 0
System.out.println(«n равно » + n) ;
>
>
>
Чтобы включить проверку утверждений во время выполнения, вы должны указать опцию -еа. Например, чтобы сделать это для AssertDemo, выполните следующую команду:
java -еа AssertDemo
После компиляции и запуска, как показано выше, программа выдает следующий результат:
n равно 3
n равно 2
n равно 1
Exception in thread «main» java.lang.AssertionError
at AssertDemo.main(AssertDemo.java:17)
Исключение в потоке «main» java.lang.AssertionError в AssertDemo.main(AssertDemo.java:17)
В main () выполняются повторяющиеся вызовы метода getnum (), который возвращает целое значение. Возвращаемое значение getnum () присваивается п и затем тестируется оператором assert:
assert n > 0; // произойдет сбой, если n == 0
Этот оператор завершится сбоем, когда п будет равно нулю, что произойдет после четвертого вызова. Когда подобное случится, будет возбуждено исключение.
Как объяснялось, вы можете специфицировать сообщение, отображаемое при сбое утверждения. Например, если вы подставите
assert n > 0 : «n отрицательное!»;
в утверждение из предыдущей программы, то будет выдан такой результат:
n равно 3
n равно 2
n равно 1
Exception in thread «main» java.lang.AssertionError : n отрицательное! at AssertDemo.main(AssertDemo.java:17)
Один момент, важный для понимания утверждений — это то, что вы не должны полагаться на них для выполнения каких-либо действий программы. Причина в том, что нормальный код окончательной версии будет выполняться с отключенным механизмом проверки утверждений. Например, рассмотрим следующий вариант предыдущей программы:
// Плохой способ применения assert! ! !
class AssertDemo // получить генератор случайных чисел
static int val = 3; // Возвращает целое.
static int getnum() return val—;
>
public static void main(String args[j) int n = 0;
for(int i=0; i 0; // Плохая идея!
System.out.println(«n is » + n) ;
>
>
>
В этой версии программы вызов getnum () перемещен в оператор assert. Хотя это хорошо работает, когда механизм проверки утверждений включен, его отключение приведет к неправильной работе программы, потому что вызов getnum () никогда не произойдет! Фактически п теперь должно быть инициализировано, поскольку компилятор распознает ситуацию, что значение может не быть присвоено в операторе assert.
Утверждения — хорошее нововведение в Java, потому что оно упрощает тип проверки ошибок, который часто используется во время разработки. Так, например, до появления assert, если вы хотели проверить, что п имеет положительное значение в приведенной выше программе, то должны были написать примерно следующую последовательность кода:
Для применения assert нужна только одна строка кода. Более того, вам не придется удалять строки с assert из окончательного варианта кода.
Утверждения — Java: Автоматическое тестирование
Каждую проверку, которую мы написали для метода StringUtils.capitalize() , в тестировании принято называть утверждением (assert). Утверждения — ключевая часть тестов. Именно они проверяют работу кода:
// Первое утверждение (проверка на пустую строку) if (!"".equals(StringUtils.capitalize(""))) throw new AssertionError("Метод работает неверно!"); > // Второе утверждение (проверка на слово) if (!"Hello".equals(StringUtils.capitalize("hello"))) throw new AssertionError("Метод работает неверно!"); >
Можно заметить, что все проверки строятся одинаковым способом: условие => исключение. Java, начиная с версии 1.4., поддерживает ключевое слово assert, которое предназначено для проверки утверждений:
// Проверка сменилась с отрицательной на положительную // Утверждение считается пройденным, если условие истинно assert 4 == 2 + 2; assert "".equals(StringUtils.capitalize("")); // Опциональное сообщение об ошибке // Указывается после двоеточия assert "Hello".equals(StringUtils.capitalize("hello")) : "Ваш метод не работает!";
Синтаксис достаточно прост:
- указываем ключевое слово assert,
- пишем выражение или вызываем метод, результатом которого будет булево выражение ( true или false ),
- опционально: после двоеточия можем указать некоторое строковое сообщение, которое будет передано вместе с ошибкой. В общем случае может быть не просто строка, а любое выражение, которое возвращает что-то отличное от void .
В случае если значение после ключевого слова assert будет равно false , то возникнет ошибка (будет выброшено AssertionError ). Другими словами, assert true означает что всё хорошо, а assert false говорит об ошибке. Например, если в последнем примере метод StringUtils.capitalize(«hello») вернёт что-то отличное от Hello, то мы увидим в консоли вот такое сообщение:
java.lang.AssertionError: Ваш метод не работает! at io.hexlet.LessonTest.testMain(LessonTest.java:12) .
В первой строке мы видим какое исключение было проброшено ( java.lang.AssertionError ) и сообщение, которое мы передали вместе с ним. Кроме сообщения, выводится бектрейс, по которому можно найти сработавшее утверждение. В данном случае необходимо изучать код на 12 строке в классе io.hexlet.LessonTest .
Использование ключевого слова assert сделало наш код короче и проще для восприятия. Положительная проверка смотрится естественнее, так как это то, что мы ожидаем.
Однако, ключевое слово assert было введено только в Java 1.4., это значит что в предыдущих версиях Java вполне можно было создать переменную или метод с именем assert. Программы в которых использовались такие имена могли стать неработоспособными после введения нового ключевого слова. Для сохранения обратной совместимости было принято решение, что по умолчанию механизм использования assert будет отключен. Если нужно чтобы ваши assert срабатывали, то при запуске программы указывается аргумент командной строки -enableassertions ( -ea ).
Рассмотрим пример. Допустим у нас есть вот такой класс:
class HelloWorld public static void main(String[] args) assert 1 == 2; // заведомо ложное утверждение System.out.println("Hello world"); > >
Если мы его скомпилируем и запустим без дополнительных аргументов, то мы увидим на экране текст Hello world, несмотря на то, что 1 точно не равно 2. Чтобы включить assert нам необходимо сделать так:
-ea HelloWorld.java # Boom!
Тогда в консоли вместо надписи Hello world мы увидим сообщение об ошибке:
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Assert. Что это?
Assert — это специальная конструкция, позволяющая проверять предположения о значениях произвольных данных в произвольном месте программы. Эта конструкция может автоматически сигнализировать при обнаружении некорректных данных, что обычно приводит к аварийному завершению программы с указанием места обнаружения некорректных данных. Странная, на первый взгляд, конструкция — может завалить программу в самый неподходящий момент. Какой же в ней смысл? Давайте подумаем, что произойдет, если во время исполнения программы в какой-то момент времени некоторые данные программы стали некорректными и мы не «завалили» сразу же программу, а продолжили ее работу, как ни в чем не бывало. Программа может еще долго работать после этого без каких-либо видимых ошибок. А может в любой момент времени в будущем «завалиться» сама по известной только ей причине. Или накачать вам полный винчестер контента с гей-порносайтов. Это называется неопределенное поведение (undefined behavior) и, вопреки расхожему мнению, оно свойственно не только языкам программирования с произвольным доступом к памяти (aka C, C++). Т.к. assert завершает программу сразу же после обнаружения некорректных данных, он позволяет быстро локализировать и исправить баги в программе, которые привели к некорректным данным. Это его основное назначение. Assert’ы доступны во многих языках программирования, включая java, c#, c и python.
Какие виды assert’ов бывают?
Assert’ы позволяют отлавливать ошибки в программах на этапе компиляции либо во время исполнения. Проверки на этапе компиляции не так важны — в большинстве случаев их можно заменить аналогичными проверками во время исполнения программы. Иными словами, assert’ы на этапе компиляции являются ничем иным, как синтаксическим сахаром. Поэтому в дальнейшем под assert’ами будем подразумевать лишь проверки во время исполнения программы.
- Проверка входящих аргументов в начале функции.
// Считает факториал числа n. // Число n должно лежать в пределах от 0 до 10 включительно. int factorial(int n) < // Факториал отрицательного числа не считается assert(n >= 0); // Если n превысит 10, то это может привести либо к целочисленному // переполнению результата, либо к переполнению стэка. assert(n return factorial(n - 1) * n; > // мы 'забыли' об ограничениях функции factorial() и пытаемся вычислить // факториалы чисел от 0 до 99. // // проверка внутри factorial() любезно напомнит нам о своих ограничениях, // так что мы сможем быстро выявить и исправить этот баг. // // если бы эта проверка отсутствовала, то баг мог бы долго оставаться // незамеченным, периодически давая о себе знать переполнениями стэка и // некорректным поведением программы. for (int i = 0; i
Важно понимать, что входящие аргументы функции могут быть неявными. Например, при вызове метода класса в функцию неявно передается указатель на объект данного класса (aka this и self). Также функция может обращаться к данным, объявленным в глобальной области видимости, либо к данным из области видимости лексического замыкания. Эти аргументы тоже желательно проверять с помощью assert’ов при входе в функцию.
Если некорректные данные обнаружены на этом этапе, то код данной функции может содержать баги. Пример:
int factorial(int n) < int result = 1; for (int i = 2; i // С первого взгляда эта проверка никогда не сработает - факториал должен // быть всегда положительным числом. Но как только n превысит допустимый // предел, произойдет целочисленное переполнение. В этом случае // a[i] может принять отрицательное либо нулевое значение. // // После срабатывания этой проверки мы быстро локализуем баг и поймем, // что либо нужно ограничивать значение n, либо использовать целочисленную // арифметику с бесконечной точностью. assert(result > 0); return result; >
- Проверка данных, с которыми работает функция, внутри кода функции.
int factorial(int n) < int result = 1; while (n >1) < // Знакомая нам проверка на целочисленное переполнение. // // При ее срабатывании мы быстро определим, что эта функция должна уметь // корректно обрабатывать слишком большие n, ведущие к переполнению. // // Эта проверка лучше, чем проверка из предыдущего пункта (перед выходом // из функции), т.к. она срабатывает перед первым переполнением result, // тогда как проверка из предыдущего пункта может пропустить случай, когда // в результате переполнения (или серии переполнений) итоговое значение // result остается положительным. assert(result return result; >
Когда и где стоит использовать assert’ы?
Ответ прост — используйте assert’ы всегда и везде, где они хоть чуточку могут показаться полезными. Ведь они существенно упрощают локализацию багов в коде. Даже проверка результатов выполнения очевидного кода может оказаться полезной при последующем рефакторинге, после которого код может стать не настолько очевидным и в него может запросто закрасться баг. Не бойтесь, что большое количество assert’ов ухудшит ясность кода и замедлит выполнение вашей программы. Assert’ы визуально выделяются из общего кода и несут важную информацию о предположениях, на основе которых работает данный код. Правильно расставленные assert’ы способны заменить большинство комментариев в коде. Большинство языков программирования поддерживают отключение assert’ов либо на этапе компиляции, либо во время выполнения программы, так что они оказывают минимальное влияние на производительность программы. Обычно assert’ы оставляют включенными во время разработки и тестирования программ, но отключают в релиз-версиях программ. Если программа написана в лучших традициях ООП, либо с помощью enterprise методологии, то assert’ы вообще можно не отключать — производительность вряд ли изменится.
Когда можно обойтись без assert’ов?
Понятно, что дублирование assert’ов через каждую строчку кода не сильно улучшит эффективность отлова багов. Не существует единого мнения насчет оптимального количества assert’ов, также как и насчет оптимального количество комментариев в программе. Когда я только узнал про существование assert’ов, мои программы стали содержать 100500 assert’ов, многие из которых многократно дублировали друг друга. С течением времени количество assert’ов в моем коде стало уменьшаться. Следующие правила позволили многократно уменьшить количество assert’ов в моих программах без существенного ухудшения в эффективности отлова багов:
Можно избегать дублирующих проверок входящих аргументов путем размещения их лишь в функциях, непосредственно работающих с данным аргументом. Т.е. если функция foo() не работает с аргументом, а лишь передает его в функцию bar(), то можно опустить проверку этого аргумента в функции foo(), т.к. она продублирована проверкой аргумента в функции bar().
Можно опускать assert’ы на недопустимые значения, которые гарантированно приводят к краху программы в непосредственной близости от данных assert’ов, т.е. если по краху программы можно быстро определить местонахождение бага. К таким assert’ам можно отнести проверки указателя на NULL перед его разыменованием и проверки на нулевое значение делителя перед делением. Еще раз повторюсь — такие проверки можно опускать лишь тогда, когда среда исполнения гарантирует крах программы в данных случаях.
Вполне возможно, что существуют и другие способы, позволяющие уменьшить количество assert’ов без ухудшения эффективности отлова багов. Если вы в курсе этих способов, делитесь ими в комментариях к данному посту.
Когда нельзя использовать assert’ы?
Т.к. assert’ы могут быть удалены на этапе компиляции либо во время исполнения программы, они не должны менять поведение программы. Если в результате удаления assert’а поведение программы может измениться, то это явный признак неправильного использования assert’а. Таким образом, внутри assert’а нельзя вызывать функции, изменяющие состояние программы либо внешнего окружения программы. Например, следующий код неправильно использует assert’ы:
// Захватывает данный мютекс. // // Возвращает 0, если невозможно захватить данный мютекс из-за следующих причин: // - мютекс уже был захвачен. // - mtx указывает на некорректный объект мютекса. // Возвращает 1, если мютекс успешно захвачен. int acquire_mutex(mutex *mtx); // Освобождает данный мютекс. // // Возвращает 0, если невозможно освободить данный мютекс из-за следующих // причин: // - мютекс не был захвачен. // - mtx указывает на некорректный объект мютекса. // Возвращает 1, если мютекс успешно захвачен. int release_mutes(mutex *mtx); // Убеждаемся, что мютекс захвачен. assert(acquire_mutex(mtx)); // Работаем с данными, "защищенными" мютексом. process_data(data_protected_by_mtx); // Убеждаемся, что мютекс освобожден. assert(release_mutes(mtx));
Очевидно, что данные могут оказаться незащищенными при отключенных assert’ах.
Чтобы исправить эту ошибку, нужно сохранять результат выполнения функции во временной переменной, после чего использовать эту переменную внутри assert’а:
int is_success; is_success = acquire_mutex(mtx); assert(is_success); // Теперь данные защищены мютексом даже при отключенных assert'ах. process_data(data_protected_by_mtx); is_success = release_mutex(mtx); assert(is_success);
Т.к. основное назначение assert’ов — отлов багов (aka ошибки программирования), то они не могут заменить обработку ожидаемых ошибок, которые не являются ошибками программирования. Например:
// Пытается записать buf_size байт данных, на которые указывает buf, // в указанное сетевое соединение connection. // // Возвращает 0 в случае ошибки записи, возникшей не по нашей вине. Например, // произошел разрыв сетевого соединения во время записи. // Возвращает 1 в случае успешной записи данных. int write(connection *connection, const void *buf, size_t buf_size); int is_success = write(connection, buf, buf_size); // "Убеждаемся", что данные корректно записаны. assert(is_success);
Если write() возвращает 0, то это вовсе не означает, что в нашей программе есть баг. Если assert’ы в программе будут отключены, то ошибка записи может остаться незамеченной, что впоследствие может привести к печальным результатам. Поэтому assert() тут не подходит. Тут лучше подходит обычная обработка ошибки. Например:
while (!write(connection, buf, buf_size)) < // Пытаемся создать новое соединение и записать данные туда еще раз. close_connection(connection); connection = create_connection(); >
Я программирую на javascript. В нем нет assert’ов. Что мне делать?
В некоторых языках программирования отсутствует явная поддержка assert’ов. При желании они легко могут быть там реализованы, следуя следующему «паттерну проектирования»:
function assert(condition) < if (!condition) < throw "Assertion failed! See stack trace for details"; >> assert(2 + 2 === 4); assert(2 + 2 === 5);
Вообще, assert’ы обычно реализованы в различных фреймворках и библиотеках, предназначенных для автоматизированного тестирования. Иногда они там называются expect’ами. Между автоматизированным тестированием и применением assert’ов есть много общего — обе техники предназначены для быстрого выявления и исправления багов в программах. Но, несмотря на общие черты, автоматизированное тестирование и assert’ы являются не взаимоисключающими, а, скорее всего, взаимодополняющими друг друга. Грамотно расставленные assert’ы упрощают автоматизированное тестирование кода, т.к. тестирующая программа может опустить проверки, дублирующие assert’ы в коде программы. Такие проверки обычно составляют существенную долю всех проверок в тестирующей программе.
- assert
- тестирование
- программирование
- Тестирование IT-систем
- Программирование
Зачем нужно ключевое слово assert?
assert – не то же самое, что методы вроде assertTrue() из тестовых библиотек. Это зарезервированное ключевое слово, унарный оператор.
Этот оператор ничего не возвращает, а принимает проверяемое утверждение типа boolean . Если значение оказывается false , проверка утверждения считается проваленной и выбрасывается AssertionError . Это похоже на сокращенную запись пары if и throw , с фиксированным типом исключения.
В Java до версии 4 слово assert не было ключевым. Поэтому для обратной совместимости механизм проверки утверждений выключен по умолчанию – логика программы никогда не должна полагаться на assert !
Включается флагом -ea или -enableassertions команды java . Можно указывать конкретные классы и пакеты в которых включить. Есть противоположный флаг -da ( -disableassertions ), эти флаги можно использовать в комбинации.
Assertion-ы используются в основном для дополнительной проверки инвариантов состояния объекта и для подстраховки в коде, который не должен никогда вызываться. Выброшенный AssertionError обычно означает ошибку программиста.
Дополнительно у оператора assert есть синтаксис передачи параметра detailMessage в конструктор AssertionError :
assert 2*2==5 : "two times two is not five!";