W3docs

Параметризованные тесты 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 / @EmptySourcenull и пустые значенияПокрытие граничных случаев строк/коллекций

Правило: @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 делает за аннотациями. Один случай намеренно содержит ошибку, чтобы вы увидели, как отдельные строки проходят или проваливаются.

java— editable, runs on the server

Что можно извлечь из этого запуска:

  • Блок 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, когда сценарий требует уникальной настройки или отдельных утверждений; вталкивание несвязанных случаев в один параметризованный метод лишь затрудняет чтение отчёта. Для общей настройки в любом из стилей см. главу жизненный цикл теста, а полный словарь утверждений, используемых внутри каждого запуска, — в главе утверждения.

Практика

Практика
В JUnit 5 ваш тест должен запускаться один раз для каждой строки таблицы, где каждая строка содержит входную строку и ожидаемую перевёрнутую строку. Какая одна аннотация наиболее подходит для передачи этих данных встроенно?
В JUnit 5 ваш тест должен запускаться один раз для каждой строки таблицы, где каждая строка содержит входную строку и ожидаемую перевёрнутую строку. Какая одна аннотация наиболее подходит для передачи этих данных встроенно?
Was this page helpful?