Как проверить, что исключение не было выброшено
В моем модульном тесте с использованием Mockito я хочу убедиться, что NullPointerException
не был брошен.
public void testNPENotThrown{
Calling calling= Mock(Calling.class);
testClass.setInner(calling);
testClass.setThrow(true);
testClass.testMethod();
verify(calling, never()).method();
}
Мой тест настроил testClass
, установив Calling
объект и свойство, так что метод будет бросать NullPointerException
,
Я проверяю, что Calling.method() никогда не вызывается.
public void testMethod(){
if(throw) {
throw new NullPointerException();
}
calling.method();
}
Я хочу пройти неудачный тест, потому что он бросает NullPointerException
, а затем я хочу написать код, чтобы исправить это.
То, что я заметил, - то, что тест всегда проходит, поскольку исключение никогда не бросает метод теста.
4 ответа
ТЛ; др
pre-JDK8: я буду рекомендовать старый добрый
try
-catch
блок.post-JDK8: используйте AssertJ или пользовательские лямбды, чтобы утверждать исключительное поведение.
длинная история
Можно написать сам, сделай сам try
- catch
заблокировать или использовать инструменты JUnit (@Test(expected = ...)
или @Rule ExpectedException
JUnit правило особенность).
Но эти способы не так элегантны и не очень хорошо сочетаются с другими инструментами.
try
-catch
блок, вы должны написать блок вокруг тестируемого поведения и записать утверждение в блоке catch, что может быть хорошо, но многие находят, что этот стиль прерывает поток чтения теста. Также вам нужно написатьAssert.fail
в концеtry
блокировать в противном случае проверка может пропустить одну сторону утверждений; PMD, findbugs или Sonar обнаружат такие проблемы.@Test(expected = ...)
Эта функция интересна тем, что вы можете написать меньше кода, и тогда написание этого теста предположительно менее подвержено ошибкам кодирования. Но этому подходу не хватает некоторых областей.- Если тест должен проверить дополнительные вещи об исключении, такие как причина или сообщение (хорошие сообщения об исключениях действительно важны, точного типа исключения может быть недостаточно).
Кроме того, поскольку в методе заложено ожидание, в зависимости от того, как написан проверенный код, неправильная часть тестового кода может вызвать исключение, что приведет к ложному положительному тесту, и я не уверен, что PMD, findbugs или Sonar будут давать намеки на такой код.
@Test(expected = WantedException.class) public void call2_should_throw_a_WantedException__not_call1() { // init tested tested.call1(); // may throw a WantedException // call to be actually tested tested.call2(); // the call that is supposed to raise an exception }
ExpectedException
Правило также является попыткой исправить предыдущие предостережения, но его использование немного неудобно, так как в нем используется стиль ожидания, пользователи EasyMock хорошо знают этот стиль. Для некоторых это может быть удобно, но если вы следуете принципам Поведенческого развития (BDD) или Arrange Act Assert (AAA),ExpectedException
Правило не вписывается в стиль письма. Помимо этого он может страдать от той же проблемы, что и@Test
Кстати, в зависимости от того, где вы размещаете ожидание.@Rule ExpectedException thrown = ExpectedException.none() @Test public void call2_should_throw_a_WantedException__not_call1() { // expectations thrown.expect(WantedException.class); thrown.expectMessage("boom"); // init tested tested.call1(); // may throw a WantedException // call to be actually tested tested.call2(); // the call that is supposed to raise an exception }
Даже ожидаемое исключение помещается перед оператором теста, оно нарушает поток чтения, если тесты следуют BDD или AAA.
Также см. Этот вопрос комментария на JUnit автора
ExpectedException
,
Таким образом, эти вышеупомянутые опции имеют всю их нагрузку и явно не защищены от ошибок кодера.
Есть проект, о котором я узнал после создания этого ответа, который выглядит многообещающим, это ловушка-исключение.
Как говорится в описании проекта, он позволяет кодировщику писать беглую строку кода, перехватывающую исключение, и предлагает это исключение для последующего утверждения. И вы можете использовать любую библиотеку утверждений, такую как Hamcrest или AssertJ.
Быстрый пример взят с домашней страницы:
// given: an empty list List myList = new ArrayList(); // when: we try to get the first element of the list when(myList).get(1); // then: we expect an IndexOutOfBoundsException then(caughtException()) .isInstanceOf(IndexOutOfBoundsException.class) .hasMessage("Index: 1, Size: 0") .hasNoCause();
Как вы видите, код действительно прост, вы ловите исключение в конкретной строке,
then
API - это псевдоним, который будет использовать API AssertJ (аналогично использованиюassertThat(ex).hasNoCause()...
). В какой-то момент проект опирался на FEST-Assert, предка AssertJ. РЕДАКТИРОВАТЬ: Похоже, что проект готовит поддержку Java 8 Lambdas.В настоящее время эта библиотека имеет два недостатка:
На момент написания этой статьи стоит отметить, что эта библиотека основана на Mockito 1.x, поскольку она создает макет тестируемого объекта за сценой. Поскольку Mockito все еще не обновлен, эта библиотека не может работать с финальными классами или финальными методами. И даже если бы он был основан на mockito 2 в текущей версии, это потребовало бы объявить глобального создателя макетов (
inline-mock-maker
), то, что может и не то, что вы хотите, так как этот макетизатор имеет иные недостатки, чем обычный макетизатор.Требуется еще одна тестовая зависимость.
Эти проблемы не будут применяться, если библиотека будет поддерживать лямбды, однако функциональность будет дублироваться набором инструментов AssertJ.
Принимая все во внимание, если вы не хотите использовать инструмент catch-exception, я порекомендую старый добрый способ
try
-catch
блок, по крайней мере, до JDK7. А для пользователей JDK 8 вы можете предпочесть использовать AssertJ, поскольку он предлагает больше, чем просто утверждение исключений.С JDK8 лямбды выходят на тестовую сцену, и они оказались интересным способом заявить об исключительном поведении. AssertJ был обновлен, чтобы обеспечить хороший свободный API, чтобы утверждать исключительное поведение.
И пример теста с AssertJ:
@Test public void test_exception_approach_1() { ... assertThatExceptionOfType(IOException.class) .isThrownBy(() -> someBadIOOperation()) .withMessage("boom!"); } @Test public void test_exception_approach_2() { ... assertThatThrownBy(() -> someBadIOOperation()) .isInstanceOf(Exception.class) .hasMessageContaining("boom"); } @Test public void test_exception_approach_3() { ... // when Throwable thrown = catchThrowable(() -> someBadIOOperation()); // then assertThat(thrown).isInstanceOf(Exception.class) .hasMessageContaining("boom"); }
С почти полным переписыванием JUnit 5 утверждения были немного улучшены, они могут оказаться интересными как готовый способ правильно утверждать исключение. Но на самом деле API утверждений все еще немного плох, снаружи ничего нет
assertThrows
,@Test @DisplayName("throws EmptyStackException when peeked") void throwsExceptionWhenPeeked() { Throwable t = assertThrows(EmptyStackException.class, () -> stack.peek()); Assertions.assertEquals("...", t.getMessage()); }
Как вы заметили
assertEquals
все еще возвращаетсяvoid
и, как таковой, не допускает создания цепочек утверждений типа AssertJ.Также, если вы помните, имя конфликтует с
Matcher
или жеAssert
Будьте готовы встретить такое же столкновение сAssertions
,
Я хотел бы сделать вывод, что сегодня (2017-03-03) простота использования AssertJ, обнаруживаемый API, быстрый темп разработки и фактическая зависимость от тестирования - это лучшее решение с JDK8 независимо от инфраструктуры тестирования (JUnit или нет), предыдущие JDK должны вместо этого полагаться на try
- catch
блоки, даже если они чувствуют себя неуклюже.
Если я не понимаю вас неправильно, вам нужно что-то вроде этого:
@Test(expected = NullPointerException.class)
public void testNPENotThrown {
Calling calling= Mock(Calling .class);
testClass.setInner(calling);
testClass.setThrow(true);
testClass.testMethod();
verify(calling, never()).method();
Assert.fail("No NPE");
}
но по названию теста "NPENotThrown" я бы ожидал такого теста:
public void testNPENotThrown {
Calling calling= Mock(Calling .class);
testClass.setInner(calling);
testClass.setThrow(true);
testClass.testMethod();
try {
verify(calling, never()).method();
Assert.assertTrue(Boolean.TRUE);
} catch(NullPointerException ex) {
Assert.fail(ex.getMessage());
}
}
Другой подход может заключаться в использовании try/catch. Это немного неопрятно, но, насколько я понимаю, этот тест в любом случае будет недолговечным, так как это для TDD:
@Test
public void testNPENotThrown{
Calling calling= Mock(Calling.class);
testClass.setInner(calling);
testClass.setThrow(true);
try{
testClass.testMethod();
fail("NPE not thrown");
}catch (NullPointerException e){
//expected behaviour
}
}
РЕДАКТИРОВАТЬ: я спешил, когда я написал это. Я имею в виду, что "этот тест в любом случае будет недолговечным, поскольку он предназначен для TDD", вы говорите, что собираетесь написать некоторый код для немедленного исправления этого теста, поэтому он никогда не вызовет исключение NullPointerException в будущем. Тогда вы можете также удалить тест. Следовательно, вероятно, не стоит тратить много времени на написание красивого теста (отсюда и мое предложение:-))
В более общем смысле:
Начиная с теста, чтобы утверждать, что (например) возвращаемое значение метода не является нулевым, является установленным принципом TDD, и проверка на наличие NullPointerException (NPE) является одним из возможных способов достижения этой цели. Однако ваш производственный код, по-видимому, не будет иметь потока, в котором создается NPE. Вы собираетесь проверить на ноль, а затем сделать что-то разумное, я думаю. Это сделало бы этот конкретный тест избыточным в тот момент, когда он будет проверять, что NPE не брошен, хотя на самом деле этого никогда не произойдет. Затем вы можете заменить его тестом, который проверяет, что происходит при обнаружении нулевого значения: возвращает, например, объект NullObject или генерирует исключение другого типа, в зависимости от того, что подходит.
Конечно, нет необходимости удалять избыточный тест, но если вы этого не сделаете, он будет сидеть там, делая каждую сборку немного медленнее и вызывая удивление у каждого разработчика, читающего этот тест; "Хм, NPE? Конечно, этот код не может бросить NPE?". Я видел много кода TDD, где у тестовых классов есть много избыточных тестов, подобных этому. Если позволяет время, то стоит регулярно проверять ваши тесты.
Обычно каждый тестовый пример запускается с новым экземпляром, поэтому установка переменной экземпляра не поможет. Так сделайthrow
переменная статическая, если нет.