Как убедиться, что исключение не было выбрано

В моем unit test с использованием 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.

I проверьте, что Calling.method() никогда не вызывается.

public void testMethod(){
    if(throw) {
        throw new NullPointerException();
    }

    calling.method();
}

Я хочу иметь неудачный тест, потому что он выбрасывает NullPointerException, а затем я хочу написать код, чтобы исправить это.

Я заметил, что тест всегда проходит, поскольку исключение никогда не вызывает метод проверки.

Ответ 1

TL;DR

  • pre-JDK8: Я рекомендую старый добрый блок try - catch.

  • post-JDK8: используйте AssertJ или настраиваемые lambdas для подтверждения исключительного поведения.

длинная история

Можно написать сами себе try - catch сделать блок или использовать инструменты JUnit (@Test(expected = ...) или @Rule ExpectedException JUnit rule).

Но эти способы не настолько элегантны и не смешивают хорошо читаемость с другими инструментами.

  • Блок 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.

    Также см. этот comment вопрос об JUnit автора ExpectedException.

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

  1. Там был проект, о котором мне стало известно после создания этого ответа, который выглядит многообещающим, catch-exception.

    Как говорится в описании проекта, он позволяет кодеру писать в беглой строке кода, улавливая исключение и предлагая это исключение для последующего утверждения. И вы можете использовать любую библиотеку утверждений, например 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();
    

    Как вы можете видеть, код действительно прост, вы поймаете исключение в определенной строке, API then - это псевдоним, который будет использовать API AssertJ (аналогично использованию assertThat(ex).hasNoCause()...). В какой-то момент проект опирался на FEST-Assert предка AssertJ. EDIT: Кажется, что проект запустил поддержку Java 8 Lambdas.

    В настоящее время эта библиотека имеет два недостатка:

    • На момент написания этой статьи стоит отметить, что эта библиотека основана на Mockito 1.x, поскольку она создает макет тестируемого объекта за сценой. Поскольку Mockito до сих пор не обновляется , эта библиотека не может работать с окончательными классами или конечными методами. И даже если бы он был основан на mockito 2 в текущей версии, для этого потребовалось бы объявить глобальный mock maker (inline-mock-maker), что может быть не так, как вы хотите, поскольку у этого mockmaker есть другие недостатки, которые обычный mockmaker.

    • Для этого требуется еще одна тестовая зависимость.

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

    Принимая во внимание все, если вы не хотите использовать инструмент исключения исключений, я порекомендую старый хороший способ блока try - catch, по крайней мере, до JDK7. И для пользователей JDK 8, которые вы, возможно, предпочтете использовать AssertJ, поскольку он предлагает больше, чем просто утверждение исключений.

  2. В 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");
    }
    
  3. При почти полной перезаписи 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, даже если они чувствуют себя неуклюжими.

Ответ 2

Если я не понимаю вас неправильно, вам нужно что-то вроде этого:

@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());
    }
}

Ответ 3

Другим подходом может быть использование 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
  }
}

EDIT: Я спешил, когда написал это. Я имею в виду, что "этот тест будет недолговечным, так как он для TDD" заключается в том, что вы говорите, что собираетесь написать код, чтобы исправить этот тест сразу, поэтому он никогда не будет генерировать исключение NullPointerException в будущем. Затем вы можете удалить тест. Следовательно, вероятно, не стоит тратить много времени на красивое испытание (отсюда и мое предложение: -))

В более общем плане:

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

Конечно, нет требования о том, чтобы вы удаляли избыточный тест, но если вы этого не сделаете, он будет сидеть там, делая каждую сборку немного медленнее и заставляя каждого разработчика, который читает тест, удивляться; "Хм, NPE? Конечно, этот код не может выбросить NPE?". Я видел много TDD-кода, где тестовые классы имеют много избыточных тестов, подобных этому. Если время позволяет, он платит, чтобы проверять ваши тесты так часто.

Ответ 4

Обычно каждый тестовый сцена выполняется с новым экземпляром, поэтому установка переменной экземпляра не поможет. Поэтому make 'throw' переменная static, если нет.