Переход от JUnit4 к новой версии во многом изменил способ расширения функциональных возможностей тестов. Напомню, что в JUnit4 основным механизмом расширения были правила (Rule), которые могли обернуть выполнение теста в дополнительную логическую обработку (например, в реализации абстрактного класса ExternalResource встраивали два дополнительных вызова методов инициализации (который также мог возвращать объект для взаимодействия с создаваемым окружением, например обертку вокруг Android Activity) и финализации (вызывается после выполнения теста и используется для очистки ресурсов). Модель JUnit 5 существенно дополнена и в этой статье мы рассмотрим как можно создавать собственные расширения для JUnit Platform.
Начнем с рассмотрения простого примера и для сравнения возьмем пример расширения для JUnit 4. Создадим функцию с одним статическим методом и напишем простой тест для нее:
package org.example; public class Main { public static int sum(int a, int b) { return a+b; } }
import org.example.Main; import org.junit.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; public class MainTest { @Test public void testSum() { assertThat(Main.sum(3,5), equalTo(8)); } }
В build.gradle добавим зависимости для junit4 и укажем его как тестовый движок (а также будем использовать hamcrest для описания тестовых утверждений):
plugins { id 'java' } group 'org.example' version '1.0-SNAPSHOT' repositories { mavenCentral() } dependencies { testImplementation 'org.hamcrest:hamcrest-core:2.2' testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:hamcrest-library:2.2' } test { useJUnit() }
Теперь сделаем дополнение, которое позволит извлекать тестовые данные и ожидаемые результаты выполнения функции из внешнего файла.
class TestData { int num1; int num2; int sum; TestData(int num1, int num2, int sum) { this.num1 = num1; this.num2 = num2; this.sum = sum; } }
class FileSourceRule implements TestRule { ArrayList<TestData> data = new ArrayList<>(); InputStream inputStream; FileSourceRule(InputStream inputStream) { this.inputStream = inputStream; } @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { //load data BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); reader.lines().forEach((Consumer) o -> { String[] split = o.toString().split(" "); int num1 = Integer.parseInt(split[0]); int num2 = Integer.parseInt(split[1]); int sum = Integer.parseInt(split[2]); data.add(new TestData(num1, num2, sum)); }); base.evaluate(); inputStream.close(); } }; } }
Альтернативно можно использовать базовый абстрактный класс ExternalResource:
class FileSourceRule extends ExternalResource { ArrayList<TestData> data = new ArrayList<>(); InputStream inputStream; FileSourceRule(InputStream inputStream) { this.inputStream = inputStream; } @Override protected void before() throws Throwable { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); reader.lines().forEach((Consumer) o -> { String[] split = o.toString().split(" "); int num1 = Integer.parseInt(split[0]); int num2 = Integer.parseInt(split[1]); int sum = Integer.parseInt(split[2]); data.add(new TestData(num1, num2, sum)); }); } @Override protected void after() { try { inputStream.close(); } catch (IOException e) { throw new RuntimeException(e); } } }
Теперь можно добавить созданное правило к нашему тесту и получить возможность загружать и использовать тестовые данные из внешнего файла (расположен в src/test/resources):
public class MainTest { @Rule public FileSourceRule rule = new FileSourceRule(ClassLoader.getSystemResourceAsStream("test.dat")); @Test public void testSum() { rule.data.forEach(data -> assertThat(Main.sum(data.num1, data.num2), equalTo(data.sum))); } }
Теперь перейдем к рассмотрению расширений в JUnit Platform и попробуем реализовать эту задачу новым способом. Прежде всего нужно отметить, что новый JUnit Platform предполагает возможность создания разных представлений для описания тестов и возможно как использование синтаксиса JUnit 4 (с установленным JUnit Vintage) или нового синтаксиса JUnit 5, но также возможны совсем другие реализации тестовых движков (например те, которые читают текстовые файлы и запускают запросы к API в соответствии с описанным сценарием). Сейчас мы рассмотрим только модель расширений JUnit5.
Добавим поддержку JUnit5 в gradle:
plugins { id 'java' } group 'org.example' version '1.0-SNAPSHOT' repositories { mavenCentral() } dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } test { useJUnitPlatform() }
И перепишем тест на использование собственных утверждений junit-jupiter:
import org.example.Main; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class TestMain { @Test void testSum() { assertEquals(Main.sum(3,5), 8); } }
Расширения в JUnit 5 реализуются с использованием базового интерфейса-маркера Extension
и регистрируются через аннотацию @ExtendWith(ExtensionClass.class)
с переопределением методов жизненного цикла:
-
before*
,after*
, где вместо * может использоваться All для однократного выполнения,Each
для каждого теста,TestExecution
для выполнения теста — интерфейсыBefore*Callback
иAfter*Callback
; -
определение условий выполнения теста через переопределение
evaluateExecutionCondition
— интерфейсExecuteCondition
-
получение значений для параметризированных тестов через
supportsParameter
/resolveParameter
— в интерфейсеParameterResolver
-
наблюдение за результатами тестов (
testDisabled
,testSuccessful
,testAborted
,testFailed
) — в интерфейсеTestWatcher
. -
многократный запуск (или запуск параметрических тестов) —
supportsTestTemplate
(поддержка запуска в нескольких контекстах) иprovideTestTemplateInvocationContexts
(поставщик контекстов) — в интерфейсеTestTemplateInvocationContextProvider
В нашем случае для реализации загрузки данных до теста можно использовать переопределение beforeAll
(для инициализации массива данных) и afterAll
(для финализации), но для начала добавим более простую реализацию логирования запуска и завершения тестов.
class LogExtension implements BeforeEachCallback, AfterEachCallback, TestWatcher { @Override public void afterEach(ExtensionContext context) { System.out.println("Finished test " + context.getDisplayName()); } @Override public void beforeEach(ExtensionContext context) { System.out.println("Started test " + context.getDisplayName()); } @Override public void testSuccessful(ExtensionContext context) { System.out.println("Test OK: "+context.getRequiredTestMethod().getName()); } }
Для подключения расширения добавим аннотацию @ExtendWith(LogExtension.class)
перед определением класса теста (или тестового метода). Теперь попробуем добавить реализацию метода для загрузки данных из внешнего источника:
class FileSourceExtension implements BeforeAllCallback, AfterAllCallback { ArrayList<TestData> data = new ArrayList<>(); InputStream inputStream; FileSourceExtension(InputStream inputStream) { this.inputStream = inputStream; } @Override public void beforeAll(ExtensionContext context) throws Exception { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); reader.lines().forEach((Consumer) o -> { String[] split = o.toString().split(" "); int num1 = Integer.parseInt(split[0]); int num2 = Integer.parseInt(split[1]); int sum = Integer.parseInt(split[2]); data.add(new TestData(num1, num2, sum)); }); } @Override public void afterAll(ExtensionContext context) throws Exception { inputStream.close(); } }
Но здесь возникнет проблема при установке через @ExtendWith
, поскольку предполагается передача значения в конструктор и нужно сохранить объект-расширения для доступа к данным. В этом случае можно использовать аннотацию @RegisterExtension
перед статическим полем с созданием объекта (аналогично аннотации @Rule
для JUnit4):
class FileSourceExtension implements BeforeAllCallback, AfterAllCallback { ArrayList<TestData> data = new ArrayList<>(); InputStream inputStream; FileSourceExtension(InputStream inputStream) { System.out.println("File source extension : " + inputStream); this.inputStream = inputStream; } @Override public void beforeAll(ExtensionContext context) { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); reader.lines().forEach((Consumer) o -> { String[] split = o.toString().split(" "); int num1 = Integer.parseInt(split[0]); int num2 = Integer.parseInt(split[1]); int sum = Integer.parseInt(split[2]); data.add(new TestData(num1, num2, sum)); }); } @Override public void afterAll(ExtensionContext context) throws Exception { inputStream.close(); } }
Теперь применим расширение к нашему тестовому классу через аннотацию статического поля:
@ExtendWith(LogExtension.class) public class TestMain { @RegisterExtension static FileSourceExtension dataSource = new FileSourceExtension(ClassLoader.getSystemResourceAsStream("test.dat")); @Test void testSum() { dataSource.data.forEach(data -> assertEquals(Main.sum(data.num1, data.num2), data.sum)); } }
Для обмена данными методы расширения могут использовать store (доступен в ExtensionContext) и взаимодействовать с контекстом запуска тестов (например, получать текущий тестовый класс/метод).
Кроме поставки данных через параметры вызова метода также может быть создано расширение для генерации значений как параметров теста:
class FileSourceExtension implements BeforeAllCallback, AfterAllCallback, ParameterResolver { ArrayList<TestData> data = new ArrayList<>(); Iterator<TestData> iterator = data.iterator(); TestData current; InputStream inputStream; FileSourceExtension(InputStream inputStream) { this.inputStream = inputStream; } @Override public void beforeAll(ExtensionContext context) { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); reader.lines().forEach((Consumer) o -> { String[] split = o.toString().split(" "); int num1 = Integer.parseInt(split[0]); int num2 = Integer.parseInt(split[1]); int sum = Integer.parseInt(split[2]); data.add(new TestData(num1, num2, sum)); }); iterator = data.iterator(); current = iterator.next(); } @Override public void afterAll(ExtensionContext context) throws Exception { inputStream.close(); } @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { return true; } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { switch (parameterContext.getParameter().getName()) { case "arg0": return current.num1; case "arg1": return current.num2; case "arg2": int sum = current.sum; if (iterator.hasNext()) { current = iterator.next(); } return sum; default: return null; } } }
Однако это не решит проблему множественного выполнения теста с различными наборами данных и тест будет выполнен однократно с первой записью из файла. Для поддержки многократного выполнения необходимо обозначить тестовый метод аннотацией @TestTemplate
и добавить в расширение реализацию методов интерфейса TestTemplateInvocationContextProvider
@ExtendWith(LogExtension.class) public class TestMain { @RegisterExtension static FileSourceExtension dataSource = new FileSourceExtension(ClassLoader.getSystemResourceAsStream("test.dat")); @TestTemplate void testSum(int num1, int num2, int sum) { System.out.println(num1 + ":" + num2 + " : " + sum); } }
class FileSourceExtension implements BeforeAllCallback, AfterAllCallback, ParameterResolver, TestTemplateInvocationContextProvider { //... @Override public boolean supportsTestTemplate(ExtensionContext context) { return true; } @Override public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) { return IntStream.range(0, data.size()).mapToObj((i) -> new ParametrizedTextInvocationContext(i)); } } } class ParametrizedTextInvocationContext implements TestTemplateInvocationContext { int count; ParametrizedTextInvocationContext(int count) { this.count = count; } @Override public String getDisplayName(int invocationIndex) { return "Iteration "+count; } }
Для контекста также может быть задано отображаемое имя (например, можно показывать номер итерации), оно будет отображаться в тестовом отчете. Также методы расширения могут взаимодействовать со сгенерированным отчетом через вызовы context.publishReportEntry
. После запуска теста отчет может выглядеть подобным образом:
Мы рассмотрели основные вопросы создания расширений для JUnit и я надеюсь, что на основе изученных примеров вы сможете реализовать любые сложные сценарии тестирования и подготовки тестовых данных.
Статья подготовлена в преддверии старта курса Java QA Engineer. Professional. Также хочу поделиться с вами записью бесплатного вебинара по теме «Пишем тесты с использованием Selenide».
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/713908/
Добавить комментарий