3 применения исключений, которые улучшат навыки программирования на Java

В статье мы узнаем:

1. Как выбрасывать исключение в пустом классе «Optional».
2. Как тестировать и просматривать исключение.
3. Как использовать ошибки утверждения.

1. Как выбрасывать исключение в пустом классе «Optional»

Не следует использовать ifPresentOrElse для исключений. Вот как это обычно происходит (плохой пример применения пользовательских исключений времени выполнения с ifPresentOrElse):

dao.findExample(id).ifPresentOrElse(this::workWithExample, () -> throw new CustomRuntimeException());

В чем проблема с этим фрагментом кода?

  • Выбрасывает только непроверяемые исключения.

emptyAction выбрасывает только RuntimeException. В качестве примера можно привести NullPointerException (исключение нулевого указателя), когда значение или действие отсутствует. Runnable выбрасывает непроверяемые исключения и не предназначен для работы с проверяемыми исключениями.

  • Исключения усложняют ifPresentOrElse.

Сценарии использования emptyAction не должны предусматривать выбрасывание исключений. Выбрасывание исключений из emptyAction приводит к усложнению кода.

У этой проблемы есть два решения.

Первое  —  использовать if для проверки наличия Optional. Затем выбросить исключение в ветви else.

Более лучшим решением будет orElseThrow. Необходимо указать поставщика исключений, например конструктор исключений.

Вот как правильно выбрасывать пользовательские проверяемые исключения:

dao.findExample(id).orElseThrow(ExampleNotFoundException::new);

2. Как тестировать и просматривать исключение

Исключения тестируют несколькими способами. Решение зависит от используемой версии JUnit и от того, что именно нужно протестировать.

Один из вариантов  —  задействовать expected, работающий с JUnit 4. Но из JUnit 5 он был убран. (Хотя это неточно: expected все еще доступен в org.junit.Test, но его никогда не существовало нигде в org.junit.jupiter).

Вокруг expected много споров. В связи с последними изменениями его убирают. То есть придется провести рефакторинг всех тестов, где используется expected. А это большая работа, так что лучше подготовиться к JUnit 5.

Использование expected казалось более чистым, и при этом создаются подробные тесты. Однако с ним нельзя просматривать исключения и нельзя тестировать более одного исключения. Поэтому возникла потребность в чем-то лучшем. Вот как выглядит expected внутри аннотации теста:

@Test(expected = CustomException.class) 
public void test() { 
  shouldThrowCustomException(); 
}

Что же использовать с последней версией JUnit после того, как от туда был убран expected? Метод assertThrows. Вот пример того, как в более новых версиях JUnit используется assertThrows:

@Test
void exceptionTesting() {
    CustomException thrown = assertThrows(
           MyException.class,
          this::shouldThrowCustomException,
           "Expected shouldThrowCustomException to throw, but it didn't"
    );

    assertTrue(thrown.getMessage().contains("Your Custom Message"));
}

Перехват исключения осуществляется после вызова исполняемого файла. Пользовательское исключение  —  первый аргумент  —  подтверждает ожидаемый тип исключения. Исполняемый файл  —  второй аргумент  —  выбрасывает исключение. Сообщение об ошибке  —  третий аргумент  —  выводится, если исключение не было выброшено.

Более подробную информацию об assertThrows можно найти здесь.

А как быть, если вы не используете JUnit 5? Задействуйте в этом случае идиому try-catch. Она применяется как альтернатива, и до появления expected обычно использовали ее.

Никакого фактического преимущества в сравнении с этим решением использование expected не дает. Разве что получается меньше строк кода, вот и все. При таком подходе есть возможность просматривать исключение. В то время как с expected этого делать нельзя. Используйте этот подход, с ним будет легче перейти на JUnit 5. До появления assertThrows такой подход был нормой, и даже сегодня это хорошая альтернатива:

// источник - https://github.com/junit-team/junit4/wiki/Exception-testing#trycatch-idiom
@Test
public void testExceptionMessage() {
  List<Object> list = new ArrayList<>();
    
  try {
    list.get(0);
    fail("Expected an IndexOutOfBoundsException to be thrown");
  } catch (IndexOutOfBoundsException anIndexOutOfBoundsException) {
    assertThat(anIndexOutOfBoundsException.getMessage(), is("Index: 0, Size: 0"));
  }
}

Еще одной альтернативой является ExpectedException. Она подойдет для работы с более старыми версиями JUnit (JUnit < 4.13). Для тестирования исключения надо добавить аннотацию Rule. Вот пример (источник):

// источник - https://github.com/junit-team/junit4/wiki/Exception-testing#expectedexception-rule
@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void shouldTestExceptionMessage() throws IndexOutOfBoundsException {
  List<Object> list = new ArrayList<Object>();
 
  thrown.expect(IndexOutOfBoundsException.class);
  thrown.expectMessage("Index: 0, Size: 0");
  list.get(0); // выполнение никогда не пойдет дальше этой строки
}

А вот мне аннотация Rule совсем не нравится. Почему здесь должна быть именно она? Только вводит в заблуждение неискушенных пользователей, даже имея более чистый код. ExpectedException в более новых версиях JUnit уже не используется.

3. Как использовать ошибки утверждения

Утверждения применяют во многих случаях: ​при тестировании, в тестовых сценариях. А когда они не выполняются, возникают ошибки утверждения.

Какой самый универсальный подход к утверждениям? Как использовать их вне тестовых сценариев? Следует ли применять ошибки утверждения в обычных классах?

Вот хорошее объяснение (источник):

Ошибки утверждения  —  это ошибки, а не исключения.

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

Вот пример ошибки утверждения AssertionError из 2-го издания книги «Java. Эффективное программирование»:

class Example {
    private Example() {
        throw new AssertionError();
    }
}

В Example имеется закрытый конструктор. Вызывать его ни в коем случае не следует. Если вызов каким-то образом происходит, совершается невозможное действие. Лучший способ передать это  —  задействовать AssertionError.

Несколько советов:

  • Усвойте разницу между Errors (ошибками) и Exceptions (исключениями). Это понимание положительно отразится на коде и облегчит вам жизнь как разработчику.
  • Просматривайте Exceptions (исключения). Такие проверки полезны, вы должны об этом знать. Берите на вооружение новые подходы к просмотру исключений. Разбирайтесь, в чем они лучше, чем старые.
  • Используйте класс Optional правильно: не злоупотребляйте, но и не ограничивайте его применение. Разберитесь с тем, как выбрасывать исключения в пустых классах Optional.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Miloš Živković: 3 Exception Practices To Improve Your Java Skills

Предыдущая статья20 скрытых особенностей JavaScript
Следующая статьяКак находить уязвимости в коде на PHP?