Статьи

Используйте ожидаемые исключения JUnit экономно

Иногда, когда мы получаем запросы на выборку для jOOQ или других наших библиотек , люди меняют код в наших модульных тестах на более «идиоматический JUnit». В частности, это означает, что они имеют тенденцию изменять это (по общему признанию, не такой красивый код):

01
02
03
04
05
06
07
08
09
10
11
12
13
@Test
public void testValueOfIntInvalid() {
    try {
        ubyte((UByte.MIN_VALUE) - 1);
        fail();
    }
    catch (NumberFormatException e) {}
    try {
        ubyte((UByte.MAX_VALUE) + 1);
        fail();
    }
    catch (NumberFormatException e) {}
}

… В эту «лучшую» и «более чистую» версию:

1
2
3
4
5
6
7
8
9
@Test(expected = NumberFormatException.class)
public void testValueOfShortInvalidCase1() {
    ubyte((short) ((UByte.MIN_VALUE) - 1));
}
 
@Test(expected = NumberFormatException.class)
public void testValueOfShortInvalidCase2() {
    ubyte((short) ((UByte.MAX_VALUE) + 1));
}

Что мы получили?

Ничего такого!

Конечно, мы уже должны использовать аннотацию @Test , так что мы могли бы также использовать ее expected атрибут, верно? Я утверждаю, что это совершенно неправильно. По двум причинам. И когда я говорю «два», я имею в виду «четыре»:

1. На самом деле мы ничего не получаем с точки зрения количества строк кода

Сравните семантически интересные биты:

01
02
03
04
05
06
07
08
09
10
11
12
// This:
try {
    ubyte((UByte.MIN_VALUE) - 1);
    fail("Reason for failing");
}
catch (NumberFormatException e) {}
 
// Vs this:
@Test(expected = NumberFormatException.class)
public void reasonForFailing() {
    ubyte((short) ((UByte.MAX_VALUE) + 1));
}

Дайте или возьмите форматирование пробелов, есть точно такое же количество важных семантических частей информации:

  1. Вызов метода для ubyte() , который находится в стадии тестирования. Это не меняет
  2. Сообщение, которое мы хотим передать в отчет об ошибке (в строке и в имени метода)
  3. Тип исключения и тот факт, что это ожидается

Таким образом, даже со стилистической точки зрения это не очень значимое изменение.

2. В любом случае, мы должны будем вернуть его обратно

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

01
02
03
04
05
06
07
08
09
10
// This:
try {
    ubyte((UByte.MIN_VALUE) - 1);
    fail("Reason for failing");
}
catch (NumberFormatException e) {
    assertEquals("some message", e.getMessage());
    assertNull(e.getCause());
    ...
}

3. Один вызов метода не является единицей

testValueOfIntInvalid() тест назывался testValueOfIntInvalid() . Таким образом, семантическая «единица», которая тестируется, является поведением valueOf() типа UByte в случае неверного ввода в целом . Не для одного значения, такого как UByte.MIN_VALUE - 1 .

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

Услышьте это, люди TDD. Я НИКОГДА не хочу включать мой дизайн API или мою логику в некоторые странные ограничения, налагаемые вашей «обратной» тестовой средой (ничего личного, JUnit). НИКОГДА ! «Мой» API в 100 раз важнее «ваших» тестов. Это включает в себя меня, не желая:

  • Сделайте все публичным
  • Сделайте все не финальным
  • Сделать все инъекционным
  • Сделайте все нестатичным
  • Используйте аннотации. Я ненавижу аннотации.

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

Не навязывайте мой дизайн или семантическое уродство из моего кода из-за тестирования.

ХОРОШО. Я слишком остро реагирую. Я всегда, при наличии аннотаций . Потому как…

4. Аннотации всегда плохой выбор для структурирования потока управления

Снова и снова меня удивляет количество злоупотреблений аннотациями в экосистеме Java. Аннотации хороши для трех вещей:

  1. Обрабатываемая документация (например, @Deprecated )
  2. Пользовательские «модификаторы» для методов, членов, типов и т. Д. (Например, @Override )
  3. Аспектно-ориентированное программирование (например, @Transactional )

И имейте в @Transactional что @Transactionalодин из очень немногих действительно полезных аспектов, которые когда-либо @Transactional в мейнстрим ( @Transactional – это еще один, или внедрение зависимостей, если вам абсолютно необходимо). В большинстве случаев AOP является нишевым методом решения проблем, и вы обычно не хотите этого в обычных программах.

Это, безусловно, НЕ хорошая идея моделировать структуры потока управления, не говоря уже о тестовом поведении, с аннотациями

Да. Java прошла долгий (медленный) путь, чтобы охватить более сложные идиомы программирования. Но если вы действительно расстроены многословием случайного выражения try { .. } catch { .. } в своих модульных тестах, то для вас есть решение. Это Java 8.

Как сделать это лучше с Java 8

Юнит лямбда находится в разработке: http://junit.org/junit-lambda.html

И они добавили новый функциональный API в новый класс Assertions : https://github.com/junit-team/junit-lambda/blob/master/junit5-api/src/main/java/org/junit/gen5/api /Assertions.java

Все основано на Executable функциональном интерфейсе :

1
2
3
4
@FunctionalInterface
public interface Executable {
    void execute() throws Exception;
}

Этот исполняемый файл теперь можно использовать для реализации кода, который утверждается, чтобы генерировать (или не генерировать) исключение. Смотрите следующие методы в Assertions

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public static void assertThrows(Class<? extends Throwable> expected, Executable executable) {
    expectThrows(expected, executable);
}
 
public static <T extends Throwable> T expectThrows(Class<T> expectedType, Executable executable) {
    try {
        executable.execute();
    }
    catch (Throwable actualException) {
        if (expectedType.isInstance(actualException)) {
            return (T) actualException;
        }
        else {
            String message = Assertions.format(expectedType.getName(), actualException.getClass().getName(),
                "unexpected exception type thrown;");
            throw new AssertionFailedError(message, actualException);
        }
    }
    throw new AssertionFailedError(
        String.format("Expected %s to be thrown, but nothing was thrown.", expectedType.getName()));
}

Это оно! Теперь те из вас, кто возражает против многословности блоков try { .. } catch { .. } могут переписать это:

1
2
3
4
5
try {
    ubyte((UByte.MIN_VALUE) - 1);
    fail("Reason for failing");
}
catch (NumberFormatException e) {}

… в это:

1
2
expectThrows(NumberFormatException.class, () ->
    ubyte((UByte.MIN_VALUE) - 1));

И если я хочу выполнить дополнительные проверки моего исключения, я могу сделать это:

1
2
3
4
Exception e = expectThrows(NumberFormatException.class, () ->
    ubyte((UByte.MIN_VALUE) - 1));
assertEquals("abc", e.getMessage());
...

Отличная работа, команда JUnit lambda!

Функциональное программирование бьет аннотации каждый раз

Аннотации использовались из-за большой логики , в основном в средах JavaEE и Spring, которые слишком стремились перенести конфигурацию XML обратно в код Java. Это пошло не так, и приведенный здесь пример ясно показывает, что почти всегда есть лучший способ явно выписать логику потока управления как с помощью объектной ориентации, так и с помощью функционального программирования, чем с помощью аннотаций.

В случае @Test(expected = ...) , я заключаю:

Покойся с миром, expected

(в любом случае, он больше не является частью аннотации JUnit 5 @Test )