Параметризованные тесты позволяют разработчикам эффективно тестируйте свой код с range значений входных данных. В области тестирования JUnit, опытные пользователи давно сталкиваются с复杂性 реализации этих тестов. Но с выпуском JUnit 5.7 начинается новая эра параметризации тестов, предлагая разработчикам первого класса поддержку и расширенные возможности. Давайте углубимся в захватывающие возможности, которые JUnit 5.7 привносит в параметризованные тесты!
Примеры параметризации из документации JUnit 5.7
Давайте рассмотрим несколько примеров из документации:
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
Аnotation @ParameterizedTest
должен сопровождаться одной из нескольких предоставленных аннотаций-источников, описывающих, откуда брать параметры. Источник параметров часто называют “поставщик данных”.
I will not dive into their detailed description here: the JUnit user guide does it better than I could, but allow me to share several observations:
- Аннотация
@ValueSource
ограничена предоставлением только одного значения параметра. Другими словами, тестовый метод не может иметь более одного аргумента, и типы, которые можно использовать, ограничены.. - передача нескольких аргументов частично решается с помощью
@CsvSource
, который парсит каждую строку в запись, которая затем передается в качестве аргументов полями. Это может легко стать трудным для чтения с длинными строками и/или множеством аргументов. Типы, которые можно использовать, также ограничены – больше об этом稍 позже. - Все источники, объявляющие фактические значения в аннотациях, ограничены значениями, которые являются константами времени компиляции (ограничение Java-аннотаций, не JUnit).
@MethodSource
и@ArgumentsSource
предоставляют поток/коллекцию (неопределенного типа) n-кортежей, которые затем передаются в качестве аргументов метода. Поддерживается множество фактических типов для представления последовательности n-кортежей, но ни один из них не гарантирует, что они подойдут для списка аргументов метода. Этот тип источника требует дополнительных методов или классов, но не накладывает ограничений на то, где и как получить тестовые данные.
Как вы видите, доступные типы источников варьируются от простых (легких в использовании, но ограниченных по функциональности) до ultimatelly гибких, которые требуют больше кода для начала работы.
- Примечание — Это в общем признак хорошего дизайна: для основной функциональности требуется мало кода, и добавление额外的 сложности обосновано, когда оно используется для поддержки более сложного случая использования.
Того, что не seems не вписывается в этого гипотетического континуума от простого до гибкого, является @EnumSource
. Давайте рассмотрим этот не trivial пример的四 параметрических наборов с 2 значениями каждый.
- Примечание — Хотя
@EnumSource
передает значение перечисления в качестве одного параметра тестового метода, концептуально тест параметризуется полями перечисления, что не накладывает ограничений на количество параметров.
enum Direction {
UP(0, '^'),
RIGHT(90, '>'),
DOWN(180, 'v'),
LEFT(270, '<');
private final int degrees;
private final char ch;
Direction(int degrees, char ch) {
this.degrees = degrees;
this.ch = ch;
}
}
@ParameterizedTest
@EnumSource
void direction(Direction dir) {
assertEquals(0, dir.degrees % 90);
assertFalse(Character.isWhitespace(dir.ch));
int orientation = player.getOrientation();
player.turn(dir);
assertEquals((orientation + dir.degrees) % 360, player.getOrientation());
}
Подумайте только: жёстко закодированный список значений серьёзно ограничивает гибкость (нет внешних или genereted данных), в то время как количество дополнительного кода, необходимого для объявления enum
, делает эту альтернативу quite a verbose по сравнению, скажем, с @CsvSource
.
Но это только первое впечатление. Мы увидим, насколько это может быть изящно, когда используется истинная сила Java enums.
- Пояснение: Эта статья не касается проверки enums, входящих в состав вашего производственного кода. Те, конечно, пришлось бы декларировать независимо от того, как вы решили их проверять. Вместо этого она фокусируется на том, когда и как выражать ваши тестовые данные в виде enums.
Когда использовать это
Есть ситуации, когда enums работают лучше, чем альтернативы:
Множество параметров на один тест
Когда вам нужно всего один параметр, вы, вероятно, не хотите усложнять вещи сверх @ValueSource
. Но как только вам понадобятся несколько параметров — например, входные данные и ожидаемые результаты — вам придётся переходить на @CsvSource,
@MethodSource/@ArgumentsSource
или @EnumSource
.
Таким образом, enum
позволяет “пробросить” любое количество полей данных.
Итак, когда вам нужно добавить больше параметров метода теста в будущем, вы просто добавляете больше полей в существующие enums, оставляя сигнатуры методов теста без изменений. Это становится бесценным, когда вы используете ваш провайдер данных в нескольких тестах.
Для других источников необходимо использовать ArgumentsAccessor
или ArgumentsAggregator
для гибкости, которую枚举 имеют “из коробки”.
Типовая безопасность
Для разработчиков на Java это должно бытьbig.
Параметры, считываемые из CSV (файлов или literals), @MethodSource
или @ArgumentsSource
, они не дают гарантии на уровне компиляции, что количество параметров и их типы будут соответствовать сигнатуре.
Конечно, JUnit будет жаловаться на время выполнения, но забудьте о какой-либо помощи кода от вашего IDE.
То же самое, что и раньше, это накапливается, когда вы используете одни и те же параметры для нескольких тестов. Использование типобезопасного подхода было бы巨大ным выигрышем при расширении набора параметров в будущем.
Custom Types
Это в основном преимущество перед текстовыми источниками, такими как те, что читают данные из CSV — значения, закодированные в тексте, необходимо преобразовать в типы Java.
Если у вас есть пользовательский класс для создания实例 из записи CSV, вы можете сделать это с помощью ArgumentsAggregator
. Однако, ваша декларация данных по-прежнему не является типобезопасной —任何 несоответствие между сигнатурой метода и объявленными данными появится во время выполнения при “агрегации” аргументов. Не говоря уже о том, что декларация класса агрегатора добавляет больше вспомогательного кода, необходимого для вашей параметризации.
Документируемый
В отличие от других методов, исходный код перечислений имеет символы Java как для наборов параметров (экземпляров перечислений), так и для всех параметров, которые они содержат (полей перечислений). Они предоставляют простой способ прикрепить документацию в ее более естественной форме — JavaDoc.
Не то чтобы документацию нельзя было разместить и в другом месте, но она будет — по определению — находиться дальше от того, что документируется, и поэтому будет труднее найти ее и легче стать устаревшей.
Но Это Еще Не Все!
Сейчас: Перечисления. Являются. Классами.
Кажется, что многим молодым разработчикам еще предстоит осознать, насколько мощными на самом деле являются Java перечисления.
В других языках программирования они действительно являются всего лишь украшенными константами. Но в Java они представляют собой удобные маленькие реализации паттерна Flyweight с (большей частью) преимуществами полноценных классов.
Почему это хорошо?
Поведение, связанное с тестовым фикстурой
Как и любой другой класс, перечисления могут иметь добавленные к ним методы.
Это становится удобным, если параметры теста переиспользуются между тестами — одни и те же данные, просто тестируются немного по-другому. Для эффективной работы с параметрами без значительного копирования и вставки требуется разделить некоторый вспомогательный код между этими тестами.
Это не то, что не решит пара вспомогательных классов и несколько статических методов.
- Popover: Заметьте, что такой дизайн страдает от Желания свойств. Методы тестирования — или еще хуже, методы вспомогательного класса — должны были бы вытянуть данные из объектов перечисления, чтобы выполнить действия с этими данными.
Хотя это (единственный) способ в процедурном программировании, в мире объектно-ориентированного программирования мы можем сделать лучше.
Объявляя “вспомогательные” методы прямо в декларации перечисления, мы переместим код туда, где находятся данные. Или, чтобы выразиться языком ООП, вспомогательные методы станут “поведением” тестовых/fixtures, реализованных в виде перечислений. Это не только сделает код более идиomatic (вызов sensibly методов на экземплярах вместо статических методов, передающих данные), но и упростит повторное использование параметров перечислений в различных тестовых случаях.
Наследование
Перечисления могут реализовывать интерфейсы с (по умолчанию) методами. При разумном использовании это может быть использовано для обмена поведением между несколькими провайдерами данных — несколькими перечислениями.
Пример, который легко приходит на ум, — это отдельные перечисления для позитивных и негативных тестов. Если они представляют собой подобный вид тестовых/fixtures, то, скорее всего, у них есть некоторые поведенческие черты для обмена.
Разговор дешев
Давайте проиллюстрируем это на тестовом наборе гипотетического конвертера исходных кодов файлов, не слишком unlike того, который выполняет преобразование из Python 2 в Python 3.
Для того чтобы иметь реальное доверие к тому, что делает такой comprehensive инструмент, в конце концов, можно получить обширный набор входных файлов, проявляющих различные аспекты языка, и соответствующие файлы для сравнения результатов преобразования. Кроме того, необходимо verificare, какие предупреждения/ошибки предоставляются пользователю для проблемных входных данных.
Это естественная candidates для параметризованных тестов из-за большого количества образцов для проверки, но это не很适合 ни одному из простых источников параметров JUnit, так как данные somewhat сложные.
Смотри ниже:
enum Conversion {
CLEAN("imports-correct.2.py", "imports-correct.3.py", Set.of()),
WARNINGS("problematic.2.py", "problematic.3.py", Set.of(
"Using module 'xyz' that is deprecated"
)),
SYNTAX_ERROR("syntax-error.py", new RuntimeException("Syntax error on line 17"));
// Много, много других ...
@Nonnull
final String inFile;
@CheckForNull
final String expectedOutput;
@CheckForNull
final Exception expectedException;
@Nonnull
final Set expectedWarnings;
Conversion(@Nonnull String inFile, @Nonnull String expectedOutput, @NotNull Set expectedWarnings) {
this(inFile, expectedOutput, null, expectedWarnings);
}
Conversion(@Nonnull String inFile, @Nonnull Exception expectedException) {
this(inFile, null, expectedException, Set.of());
}
Conversion(@Nonnull String inFile, String expectedOutput, Exception expectedException, @Nonnull Set expectedWarnings) {
this.inFile = inFile;
this.expectedOutput = expectedOutput;
this.expectedException = expectedException;
this.expectedWarnings = expectedWarnings;
}
public File getV2File() { ... }
public File getV3File() { ... }
}
@ParameterizedTest
@EnumSource
void upgrade(Conversion con) {
try {
File actual = convert(con.getV2File());
if (con.expectedException != null) {
fail("No exception thrown when one was expected", con.expectedException);
}
assertEquals(con.expectedWarnings, getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
} catch (Exception ex) {
assertTypeAndMessageEquals(con.expectedException, ex);
}
}
Использование перечислений не ограничивает нас в том, насколько сложными могут быть данные. Как ты можешь увидеть, мы можем определить несколько удобных конструкторов в перечислениях, так что объявление новых наборов параметров получается удобным и чистым. Это предотвращает использование длинных списков аргументов, которые часто заканчиваются заполненными многими “пустыми” значениями (null, пустые строки или коллекции), которые оставляют одного в замешательстве, что на самом деле представляет собой аргумент #7 — знаешь, один из null.
Обрати внимание, как перечисления позволяют использовать сложные типы (Set
, RuntimeException
) без ограничений или магических преобразований. Передача таких данных также совершенно типобезопасна.
Теперь я знаю, о чем ты думаешь. Это ужасающе многословно. Ну, до определенной степени. Реалистично, у тебя будет гораздо больше образцов данных для проверки, так что количество типовых кодов будет менее значительным в сравнении.
Кроме того, обрати внимание, как связанные тесты могут быть написаны, используя те же перечисления и их вспомогательные методы:
@ParameterizedTest
@EnumSource
// Обновление уже обновленных файлов всегда проходит успешно, изменений не производится, предупреждений не выдается.
void upgradeFromV3toV3AlwaysPasses(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
}
@ParameterizedTest
@EnumSource
// Версия downgrade файлов, созданных процедурой обновления, ожидается, что всегда пройдет без предупреждений.
void downgrade(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV2File());
}
Ещё немного разговора после всего этого
Концептуально, @EnumSource
поощряет вас создавать сложное, машинопрочтаемое описание отдельных тестовых сценариев, размывая границу между источниками данных и тестовыми fixture.
Другая замечательная особенность использования каждого набора данных в виде символа Java (элемент enum) заключается в том, что их можно использовать по отдельности; полностью вне источников данных/тестов с параметрами. Поскольку у них есть разумное имя и они самодостаточны (в плане данных и поведения), они способствуют созданию красивых и читаемых тестов.
@Test
void warnWhenNoEventsReported() throws Exception {
FixtureXmls.Invalid events = FixtureXmls.Invalid.NO_EVENTS_REPORTED;
// read() — это вспомогательный метод, который используется всеми FixtureXmls
try (InputStream is = events.read()) {
EventList el = consume(is);
assertEquals(Set.of(...), el.getWarnings());
}
}
Теперь, @EnumSource
не будет одним из наиболее часто используемых источников аргументов, и это خوبо, так как чрезмерное его использование не принесет пользы. Но в правильных обстоятельcтвах, полезно знать, как использовать все, что они предлагают.
Source:
https://dzone.com/articles/junit5-parameterized-tests-with-enumsource