Параметризованные тесты JUnit в Java
Запускайте один JUnit-тест с разными входными данными с помощью @ParameterizedTest и источников значений.
Параметризованный тест запускает один и тот же тестовый метод многократно — по одному разу для каждого набора входных данных. Вместо того чтобы копировать testReverseAbc, testReverseEmpty и testReverseSingle, вы описываете логику один раз и передаёте источник данных — список входных значений и ожидаемых результатов. JUnit 5 (движок Jupiter) поддерживает это как первоклассную возможность с помощью @ParameterizedTest и семейства аннотаций-источников. Выигрыш — меньше кода, более плотное покрытие и отдельный результат прохождения/провала для каждого входного значения.
Эта глава предполагает, что вы уже знаете, как написать и проверить обычный тест; если нет — начните с введения в JUnit и утверждений JUnit. Здесь рассматривается, когда стоит использовать параметризованный тест, как выбрать источник аргументов (@ValueSource, @CsvSource, @MethodSource и другие), а также самая распространённая ошибка — неверное ожидаемое значение вместо реального бага в коде.
От повторяющихся тестов к одному параметризованному
Обычный метод @Test проверяет ровно один сценарий. Когда нужно проверить одно и то же поведение для таблицы входных данных, наивный подход предполагает повторение метода:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@Test void two_isPrime() { assertTrue(Primes.isPrime(2)); }
@Test void seven_isPrime() { assertTrue(Primes.isPrime(7)); }
@Test void thirteen_isPrime() { assertTrue(Primes.isPrime(13)); }
}Параметризованная версия сворачивает все три в один метод. Используйте аннотацию @ParameterizedTest (а не @Test) и укажите источник, поставляющий аргумент для каждого запуска:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@ParameterizedTest
@ValueSource(ints = {2, 7, 13})
void isPrime(int candidate) {
assertTrue(Primes.isPrime(candidate));
}
}JUnit вызывает isPrime три раза — candidate=2, затем 7, затем 13 — и сообщает три результата. Провал одного значения не скрывает остальные.
Выбор источника аргументов
Аннотация @ParameterizedTest бесполезна сама по себе — ей нужен источник, предоставляющий аргументы. JUnit Jupiter поставляется с несколькими вариантами, каждый из которых подходит для определённой формы данных:
| Источник | Предоставляет | Лучше всего для |
|---|---|---|
@ValueSource | Один литерал на запуск (ints, strings, doubles, …) | Тесты с одним аргументом |
@CsvSource | Строку значений, разделённых запятыми, на запуск | Небольшое число встроенных строк с несколькими столбцами |
@CsvFileSource | Строки из .csv-файла в classpath | Большие или внешне поддерживаемые таблицы |
@MethodSource | Всё, что возвращает фабричный метод в виде Stream/Collection | Сложные объекты, вычисляемые случаи |
@EnumSource | Константы перечисления | Полное покрытие enum |
@NullSource / @EmptySource | null и пустые значения | Покрытие граничных случаев строк/коллекций |
Правило: @ValueSource — для одного простого входного значения, @CsvSource — для небольшой многоколоночной таблицы, @MethodSource — когда данные перестают помещаться в литералы аннотации.
Несколько столбцов с @CsvSource
Когда в каждом случае есть входное значение и ожидаемый результат, @CsvSource даёт вам небольшую встроенную таблицу. Каждая строка — это одна строка данных; запятые делят её на параметры метода по порядку:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class StringsTest {
@ParameterizedTest
@CsvSource({
"abc, cba",
"racecar, racecar",
"'', ''" // single quotes denote an empty string
})
void reverse(String input, String expected) {
assertEquals(expected, Strings.reverse(input));
}
}JUnit преобразует каждый токен, разделённый запятой, в объявленный тип параметра, поэтому @CsvSource({"4, 16"}) может попасть в (int n, int square). Используйте одинарные кавычки, чтобы включить запятые или пустые строки в ячейку.
Вычисляемые случаи с @MethodSource
Значения аннотаций должны быть константами времени компиляции, поэтому когда аргументы являются настоящими объектами или требуют вычисления, переходите к @MethodSource. Он указывает статический метод, возвращающий Stream<Arguments> (или любую Collection/массив):
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TaxTest {
static Stream<Arguments> brackets() {
return Stream.of(
Arguments.of(0, 0.0),
Arguments.of(10_000, 1_000.0),
Arguments.of(50_000, 7_500.0)
);
}
@ParameterizedTest(name = "income {0} -> tax {1}")
@MethodSource("brackets")
void computesTax(int income, double expectedTax) {
assertEquals(expectedTax, Tax.of(income));
}
}Необязательный атрибут name настраивает отображение каждого вызова в отчёте о тестировании: {0}, {1} заменяются аргументами — это незаменимо, когда нужно с первого взгляда определить одну проваленную строку.
Практический пример: параметризованный запуск без JUnit
Среда выполнения кода не имеет JUnit в classpath, поэтому эта программа моделирует механизм, воплощённый параметризованным тестом, с помощью обычного кода JDK: одна проверка определяется один раз, а затем выполняется для списка случаев — именно то, что @ParameterizedTest делает за аннотациями. Один случай намеренно содержит ошибку, чтобы вы увидели, как отдельные строки проходят или проваливаются.
Что можно извлечь из этого запуска:
- Блок
reverseвыводит четыре строкиPASSи>> reverse: 4 passed, 0 failed— одно тело (reverse) выполняется для четырёх строк, отражая то, как единственный метод@ParameterizedTestвызывается один раз на каждую строку@CsvSource. - Блок
isPrimeвыводитPASSдля входных значений2,7,9и1, ноFAILдля4, потому чтоisPrime(4)возвращаетfalse, а строка утверждалаtrue— это неверное ожидание, а не ошибка в коде, что является самой распространённой ошибкой в параметризованных тестах. - Этот единственный провал сообщается на отдельной строке и считается как
>> isPrime: 4 passed, 1 failed; остальные строки по-прежнему проходят, демонстрируя ключевое преимущество перед самописным циклом с одним утверждением — каждое входное значение является независимым, отдельно сообщаемым случаем. - Вспомогательный метод
runAllпринимает проверяемый блок какFunction, а случаи — какList, разделяя логику под тестированием от данных — именно это разделение обеспечивает@ParameterizedTestвместе с источником аргументов. - Каждая строка показывает
expectedрядом сactual, поэтому строка4 / expected=true / actual=falseточно указывает, какое значение не совпало — такую же диагностическую ценность предоставляет сообщениеassertEqualsв JUnit и шаблонname = "...".
Когда использовать параметризованный тест
Используйте @ParameterizedTest, когда одно поведение должно выполняться для таблицы входных данных — граничных значений, классов эквивалентности или регрессионного списка входных данных, которые когда-то вызывали ошибки. Продолжайте использовать обычный @Test, когда сценарий требует уникальной настройки или отдельных утверждений; вталкивание несвязанных случаев в один параметризованный метод лишь затрудняет чтение отчёта. Для общей настройки в любом из стилей см. главу жизненный цикл теста, а полный словарь утверждений, используемых внутри каждого запуска, — в главе утверждения.