Знакомство с основными концепциями фреймворка тестирования JUnit 5, подключение и базовое использование.
Моя статья по-сути является вольным пересказом официальной документации. Мы с вами рассмотрим основные моменты, необходимые для начала написания собственных unit-тестов.
Обзор
Начнем немного с общих сведений.
JUnit — фреймворк из семейства xUnit фреймворков тестирования. Играет важную роль в современной test-driven разработке. Предоставляет удобные инструменты для написания эффективных тестов.
Текущая актуальная версия JUnit 5 содержит три основных модуля:
- JUnit Platform — основной компонент платформы для запуска фреймворков тестирования в jvm. Содержит описание TestingEngine API для разработки фреймворков тестирования, которые можно будет запустить на этой платформе. Так же содержит лончеры для запусков тестов, написанных под JUnit 4.
- JUnit Jupiter — содержит в себе реализацию фреймворка тестирования Jupiter и компоненты для запуска на платформе. Состоит из новой программной модели и модели расширений.
- JUnit Vintage — содержит компоненты поддержки обратной совместимости для тестов, написанных на JUnit 3 и JUnit 4.
Для запуска тестов необходима минимальная версия Java 8. Но остается возможность проверять классы, написанные на предыдущих версиях Java.
Подключение
Во-первых, необходимо подключить фреймворк к вашему проекту. Для быстрого старта достаточно подключить зависимость JUnit Jupiter:
// gradle Groovy DSL
implementation 'org.junit.jupiter:junit-jupiter:5.7.2'
<!-- maven -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
Быстрый старт
Далее рассмотрим пример простейшего теста:
import static org.junit.jupiter.api.Assertions.assertEquals;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class MyFirstJUnitJupiterTests {
private final Calculator calculator = new Calculator();
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
Аннотация org.junit.jupiter.api.Test помечает методы с тестами для фреймворка. С помощью лончера эти тесты будут запущены через платформу тестирования. В теле таких методов, описывается логика утверждений (assertions). В данном примере у класса калькулятор проверяется, что сумма двух аргументов метода сложения будет равна двум. Если возвращаемое значение не соответствует ожидаемому — то будет выброшено исключение и тест будет считаться проваленным.
Аннотации
Движок Jupiter использует аннотации для настойки тестов. С одной из аннотаций @Test мы уже успели познакомиться в примере выше. Данная аннотация указывает на тестовый метод. В случае наследования класса, данный метод будет так же наследовать эти свойства, до того пока не будет перегружен в дочерних классах. Полный список с указанием поведения при наследовании можно посмотреть в оригинале.
Тестовые классы и методы
Тестовый класс — это класс верхнего уровня, статический или вложенный с аннтоацией @Nested, который содержит хотя бы один тестовый метод с аннотацией @Test. Такие классы не должны быть абстрактными и должны содержать только один конструктор.
Тестовый метод — это методы класса (не статические), помеченные аннотацией, указывающей на тестовые методы, например @Test. Так же подойдут @RepeatedTest, @ParametrizedTest, @TestFactory или @TestTemplate.
Методы жизненного цикла — это методы помеченные аннотациями @BeforeAll, @AfterAll, @BeforeEach, или @AfterEach. Не должны быть абстрактными и не должны возвращать значения.
Перечисленные методы могут иметь любой модификатор доступа, кроме private.
Пример ниже иллюстрирует типовой тестовый класс:
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class StandardTests {
@BeforeAll
static void initAll() {
//выполняется один раз перед всеми тестами в классе
}
@BeforeEach
void init() {
//выполняется перед каждым тестом
}
@Test
void succeedingTest() {
//в данном примере допускается что в этом тесте не будет ошибок
}
@Test
void failingTest() {
//этот тест специально провален через метод fail
fail("a failing test");
}
@Test
@Disabled("for demonstration purposes") //пропускает тест
void skippedTest() {
// не будет запущен
}
@Test
void abortedTest() {
assumeTrue("abc".contains("Z")); //это утверждение неверно и тест упадет
fail("test should have been aborted"); //эта строка не будет выполнена, тест завершится выше
}
@AfterEach
void tearDown() {
//выполнится после каждого теста
}
@AfterAll
static void tearDownAll() {
//выполнится один раз после всех тестов
}
}
Assertions (Утверждения)
В JUnit Jupiter содержится большое число различных методов для проверки работы ваших тестируемых классов — assertions ( утверждения). Такие методы при неудовлетворения условий будут выбрасывать исключения и тестовые методы с такими исключениями будут помечаться как failed (проваленные). Повсеместно используются лямбда-выражения из Java 8. Ниже приведен пример применения популярных утверждений (ассертов).
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import example.domain.Person;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class AssertionsDemo {
private final Calculator calculator = new Calculator();
private final Person person = new Person("Jane", "Doe");
@Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertEquals(4, calculator.multiply(2, 2),
"The optional failure message is now the last parameter"); //можем добавлять комментарии к нашим утверждениям
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily."); //так же можем делать ленивую инициализацию комментариев через лямбду
}
@Test
void groupedAssertions() {
// В группах будут выполняться все проверки
// и упавшие проверки будут отображены в отчете вместе.
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// Внутри блока кода, если проверка упала, дальнейшие проверки
// внутри текущего блока будут пропущены. Так как будет выброшено исключение.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Выполниться только если проверка на firstName будет успешной
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Утверждения в группе выполняются независимо
// по этому данный блок будет выполняться даже если проверка на firstName упадет.
String lastName = person.getLastName();
assertNotNull(lastName);
// Выполниться если проверка на lastName будет успешной
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
//Можно проверять, что метод выбросил исключение
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
//А так же при необходимости проверить текст исключения
assertEquals("/ by zero", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// Следующая проверка пройдет
assertTimeout(ofMinutes(2), () -> {
// Выполнение операций, которые займут менее 2 минут
});
}
@Test
void timeoutNotExceededWithResult() {
// Следующая проверка пройдет с получением результата из лямбды
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
void timeoutNotExceededWithMethod() {
// Следующий пример показывает вызов метода по ссылке и получение результата
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
}
@Test
void timeoutExceeded() {
// Следующий пример упадет с похожей ошибкой:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Симуляция операции длительностью более 10 мс, код будет выполняться до конца
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// Следующий пример упадет с похожей ошибкой:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Симуляция операции длительностью более 10 мс, код будет завершен принудительно по таймауту
new CountDownLatch(1).await();
});
}
private static String greeting() {
return "Hello, World!";
}
}
Нас предупреждают, что проверка на таймаут assertTimeoutPreemptively выполняется в отдельном потоке и следует быть осторожным с кодом, который использует ThreadLocal переменные, возможны нежелательные сайдэффекты.
Assumptions (предположения)
Assumptions (предположения) — методы для проверки условий или условные проверки. В отличии от утверждений, если проверка будет провалена, то в отчете такой метод будет помечен как aborted (отменен). Ниже приведен класс с примерами использования предположений:
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class AssumptionsDemo {
private final Calculator calculator = new Calculator();
@Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// остальные утверждения в тесте
}
@Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// остальные утверждения в тесте
}
@Test
void testInAllEnvironments() {
assumingThat("CI".equals(System.getenv("ENV")),
() -> {
// условное выполнение проверок, в данном случае при выполнении условия выше
assertEquals(2, calculator.divide(4, 2));
});
// вне зависимости от предположения выше, следующее утверждение будет проверяться
assertEquals(42, calculator.multiply(6, 7));
}
}
Запуск тестов
В IntelliJ IDEA есть встроенная поддержка тестов, тестовые классы можно запускать силами самой IDE прямо из коробки.
Для системы сборки Gradle начиная с версии 4.6 присутствует нативная поддержка JUnit. Базовым описанием этапа запуска тестов является следующий пример:
test {
useJUnitPlatform()
}
Для Maven существует нативная поддержка плагином maven-surefire-plugin. Для этого достаточно подключить плагин к проекту:
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
По умолчанию плагин будет искать файлы в тестовой области видимости по шаблону наименования классов:
- **/Test*.java
- **/*Test.java
- **/*Tests.java
- **/*TestCase.java
При необходимости правила поиска файлов можно задать через директивы include и exclude.
Этой информации достаточно для уверенного старта написания ваших собственных тестов. Напоминаю, что подробнее о возможностях фреймворка можно ознакомиться в официальной документации.