Runners

Параметризованные тесты, предположения, теории и правила покрывают 95% всего необходимого для написания юнит-тестов функционала. Оставшимися пять процентами занимается тяжёлая артиллерия: собственные test runners.

Подготовка

Для иллюстрации возьмём LocalizedDateService  из примера с предположениями и перепишем его, чтобы проверять значения для нескольких локалей. Строго говоря, такой тест было бы гораздо проще и лучше написать с использованием параметров, но наша цель собственные test runners, а более-менее реалистичного простого сценария, требующего этого, придумать не удалось.

Добавим в класс сервиса знания о локалях:

LocaleHolder — это простой класс, оборачивающий статическую переменную, хранящую текущую локаль. Тоже не самое лучшее решение, но для наших нужд сойдёт.

И тест, который умеет локали:

Собственный runner

Наш runner будет выставлять локаль и передавать ожидаемое значение в тест. Опять же, эту проблему можно решить с помощью параметризованного теста, но мы не ищём лёгких путей.

Runner состоит из двух частей. В первой части мы оборачиваем Suite, что позволит нам породить несколько тестов из одного:

В runner перечислены локали и отзывы на них, реализован требуемый контрактом конструктор и, самое главное, формируется список из тестов с разными локалями.

Говоря проще, Suite — это, в терминах JUnit, набор из нескольких классов с тестами, исполняемый как один большой класс с тестами. Locales использует понятие Suite в своих грязных целях — в Suite добавляется один и тот же (фактически) класс, но с разными настройками LocalesRunner. Тем самым мы исполняем тестовый класс столько раз, сколько у нас локалей в конфиге.

Вторая часть — непосредственно runner для одного конкретного класса теста:

LocalesRunner порождён от runner’а по умолчанию BlockJUnit4ClassRunner.

Переопределённые методы getName() и testName() очевидны и служат для украшения вывода тестов.

Переопределение validatePublicVoidNoArgMethods() — это костыль. JUnit требует, чтобы тестовые методы были публичными, ничего не возвращали и не принимали никаких параметров. Базовый класс автоматически проверяет методы на соответствие этим требованиям. Но. Наш runner требует методов с параметрами, поэтому я переопределил код проверки и исключил из него проверку на отсуствие аргументов.

Переопределённый methodBlock() возвращает Statement с тестом внутри. На самом деле главным здесь будет метод evaluate(), который выставляет сохранённую в LocalesRunner локаль и вызывает тестовый метод с параметром. Обратите внимание, что в данном примере не будут работать правила, предположения, методы @Before/@After и т.д. Их поддержку я исключил чтоб не загромождать деталями код примера.

Впрочем, поддержку функционала JUnit добавить несложно, например, изменив methodBlock следующим образом:

Тест использует специальную аннотацию @RunWith для запуска с нашим runner и необычные функции с параметрами:

При запуске теста видно, что не смотря на то, что в классе теста только один тестовый метод, реально исполняется несколько тестов:

Код примера доступен на github.