Иногда, когда мы получаем запросы на выборку для 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 )); } |
Дайте или возьмите форматирование пробелов, есть точно такое же количество важных семантических частей информации:
- Вызов метода для
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
|
@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
)
Ссылка: | Экономно используйте ожидаемые исключения JUnit от нашего партнера по JCG Лукаса Эдера в блоге JAVA, SQL и AND JOOQ . |