Иногда, когда мы получаем запросы на выборку для jOOQ или других наших библиотек , люди меняют код в наших модульных тестах на более «идиоматический JUnit». В частности, это означает, что они имеют тенденцию изменять это (по общему признанию, не такой красивый код):
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
@Testpublic 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));} |
Дайте или возьмите форматирование пробелов, есть точно такое же количество важных семантических частей информации:
- Вызов метода для
ubyte(), который находится в стадии тестирования. Это не меняет - Сообщение, которое мы хотим передать в отчет об ошибке (в строке и в имени метода)
- Тип исключения и тот факт, что это ожидается
Таким образом, даже со стилистической точки зрения это не очень значимое изменение.
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. Аннотации хороши для трех вещей:
- Обрабатываемая документация (например,
@Deprecated) - Пользовательские «модификаторы» для методов, членов, типов и т. Д. (Например,
@Override) - Аспектно-ориентированное программирование (например,
@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
|
@FunctionalInterfacepublic 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 )
| Ссылка: | Экономно используйте ожидаемые исключения JUnit от нашего партнера по JCG Лукаса Эдера в блоге JAVA, SQL и AND JOOQ . |