Статьи

Тестирование на ожидаемые исключения в JUnit

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

Возьмите следующий простой фрагмент кода в качестве примера. Помимо написания тестов, чтобы гарантировать, что метод canVote () возвращает true или false, вы также должны написать тесты, чтобы убедиться, что он IllegalArgumentException когда ожидается.

1
2
3
4
5
6
7
public class Student {
      public boolean canVote(int age) {
          if (i<=0) throw new IllegalArgumentException("age should be +ve");
          if (i<18) return false;
          else return true;
      }
  }

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

Существует 3 распространенных способа проверки создания исключений, каждый из которых имеет свои преимущества и недостатки.

1) @Test (ожидается …)

Аннотация @Test имеет необязательный параметр «Ожидаемый», который позволяет указать подкласс Throwable . Если бы мы хотели убедиться, что canVote() () выше выдает правильное исключение, мы бы написали:

1
2
3
4
5
    @Test(expected = IllegalArgumentException.class)
    public void canVote_throws_IllegalArgumentException_for_zero_age() {
        Student student = new Student();
        student.canVote(0);
    }

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

2) ExpectedException

Чтобы использовать ExpectedException в JUnit, сначала необходимо объявить ExpectedException:

1
2
@Rule
    public ExpectedException thrown= ExpectedException.none();

Тогда вы можете использовать более простой подход — указать только ожидаемое исключение:

1
2
3
4
5
6
@Test
    public void canVote_throws_IllegalArgumentException_for_zero_age() {
        Student student = new Student();
        thrown.expect(NullPointerException.class);
        student.canVote(0);
    }

Или также можете указать ожидаемое сообщение об исключении:

1
2
3
4
5
6
7
@Test
    public void canVote_throws_IllegalArgumentException_for_zero_age() {
        Student student = new Student();
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("age should be +ve");
        student.canVote(0);
    }

Помимо возможности указать ожидаемое сообщение об исключении, этот подход ExpectedException имеет то преимущество, что позволяет вам быть более точным в отношении того, где ожидается генерирование исключения. В приведенном выше примере непредвиденное исключение IllegalArgumentException, созданное в конструкторе, приведет к сбою теста, поскольку мы ожидали, что он будет вызван в методе canVote ().

Кстати, было бы неплохо, если бы в объявлении не было необходимости:

1
@Rule public ExpectedException thrown= ExpectedException.none();

Это просто кажется ненужным шумом. Было бы неплохо просто быть в состоянии сделать

1
expect(RuntimeException.class)

или же

1
expect(RuntimeException.class, “Expected exception message”)

или, по крайней мере, иметь возможность передать исключение и сообщение в одном вызове ExpectedException:

1
thrown.expect(IllegalArgumentException.class, “age should be +ve”);

3) Попробуйте / поймать с утверждением / неудачей

До JUnit4 способом проверки исключений было использование блоков try / catch.

01
02
03
04
05
06
07
08
09
10
@Test
    public void canVote_throws_IllegalArgumentException_for_zero_age() {
        Student student = new Student();
        try {
            student.canVote(0);
        } catch (IllegalArgumentException ex) {
            assertThat(ex.getMessage(), containsString("age should be +ve"));
        }
        fail("expected IllegalArgumentException for non +ve age");
    }

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

В заключение, существует три основных подхода к тестированию ожидаемых исключений, каждый из которых имеет свои преимущества и недостатки. Лично я обычно склоняюсь к подходу ExpectedException из-за его точности и способности тестировать сообщение об исключении.