В минувшее воскресенье Sam Brannen анонсировал выход JUnit 5! Ура!
Поздравляю всех участников @JUnitTeam а также всех, кто использует JUnit в своей работе! Давайте посмотрим, что же нам приготовили в этом релизе.
Содержание
0. Введение
1. Начало работы
2. Обзор нововведений
2.1. public — всё
2.2. Продвинутый assert
2.3. Работа с исключениями
2.4. Новый Test
2.5. Новые базовые аннотации
2.6. Вложенные классы
2.7. Разделяемый инстанс класса для запуска тестов
2.8. Автоматический повторный запуск теста
2.9. Параметризированные тесты
2.10. Аннотированные default методы в интерфейсах
3. Заключение
1. Введение
Итак, официальный сайт начинает с того, что сообщает нам о новом строении JUnit:
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage (← офф.сайт).
JUnit Platform — фундаментальная основа для запуска на JVM фреймворков для тестирования. Платформа предоставляет TestEngine API, для разработки фреймворков (для тестирования), которые могут быть запущены на платформе. Кроме этого, в платформе имеется Console Launcher для запуска платформы из коммандной строки а также для запуска любого JUnit 4 Runner'а на платформе. Уже, кстати, есть плагины для Gradle и Maven.
JUnit Jupiter — сердце JUnit 5. Этот проект предоставляет новые возможности для написания тестов и создания собственных расширений. В проекте реализован специальный TestEngine для запуска тестов на ранее описанной платформе.
JUnit Vintage — поддержка легаси. Определяется TestEngine для запуска тестов ориентированных на JUnit 3 и JUnit 4.
1. Начало работы
В интернете уже полно примеров для настройки Gradle и Maven проектов. В блоге JetBrains есть отдельный пост, посвященный настройке JUnit 5 в IDEA.
2. Обзор нововведений
А теперь наконец-то перейдем к примерам!
2.1. public — всё
JUnit больше не требует, чтобы методы были публичными.
@Test
void test() {
assertEquals("It " + " works!" == "It works!");
}
2.2. Продвинутый assert
Опциональное сообщение сделали последним аргументом.
assertEquals(2017, 2017, "The optional assertion message is now the last parameter.");
В пятой версии для конструирования сообщения можно использовать Supplier<String>.
assertTrue("habr" == "habr", () -> "Assertion messages can be lazily evaluated");
Добавили специальный метод для логической группировки тестов.
// в группе все ассерты исполняются независимо,
// успех - когда прошли успешно все ассерты
assertAll("habr",
() -> assertThat("https://habrahabr.ru", startsWith("https")),
() -> assertThat("https://habrahabr.ru", endsWith(".ru"))
);
Появился метод для работы с Iterable.
assertIterableEquals(asList(1, 2, 3), asList(1, 2, 3));
Добавили интересный метод для сравнения набора строк. Поддерживаются регулярные выражения!
Assertions.assertLinesMatch(
asList("можно сравнивать строки", "а можно по regex: \d{2}\.\d{2}\.\d{4}"),
asList("можно сравнивать строки", "а можно по regex: 12.09.2017")
);
2.3. Работа с исключениями
Работа с исключениями стала более линейной.
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("что-то пошло не так");
});
assertEquals("что-то пошло не так", exception.getMessage());
2.4. Новый Test
JUnit 5 привнес новую аннотацию Test, которая находится в пакете org.junit.jupiter.api.Test. В отличии от четвертой версии, новая аннотация служит исключительно маркером.
// JUnit 4
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test {
Class<? extends Throwable> expected() default Test.None.class;
long timeout() default 0L;
public static class None extends Throwable {
private static final long serialVersionUID = 1L;
private None() {
}
}
}
Новая аннотация выглядит так.
// JUnit 5
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(Stable)
@Testable
public @interface Test {
}
2.5. Новые базовые аннотации
В пятой версии добавили новые базовые аннотации.
import static org.junit.jupiter.api.Assertions.fail;
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 {
// вместо @BeforeClass
@BeforeAll
static void initAll() {
}
// вместо @Before
@BeforeEach
void init() {
}
@Test
void succeedingTest() {
}
@Test
void failingTest() {
fail("a failing test");
}
// Вместо @Ignore
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
// Новая аннотация для улучшения читаемости при выводе результатов тестов.
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {}
// вместо @After
@AfterEach
void tearDown() {
}
// вместо @AfterClass
@AfterAll
static void tearDownAll() {
}
}
2.6. Вложенные классы
Аннотация @Nested позволяет использовать внутренние классы при разработке тестов, что позволяет иногда более удобным способом группировать/дополнять тесты.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, () -> stack.pop());
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, () -> stack.peek());
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
2.7. Разделяемый инстанс класса для запуска тестов
Для гарантии независимости и изоляциии тестов JUnit во всех предыдущих версиях всегда создавал по инстансу на тест (т.е. на каждый запуск метода отдельный инстанс). В пятой версии такое поведение можно изменить используя новую аннотацию @TestInstance(Lifecycle.PER_CLASS). В таком случае инстанс будет создан только один раз и будет переиспользован для запуска всех тестов, определенных внутри этого класса.
2.8. Автоматический повторный запуск теста
Еще одна приятная добавка! Аннотация @RepeatedTest сообщает JUnit, что данный тест нужно запустить несколько раз. При этом, каждый такой вызов будет независимым тестом, а значит для него будут работать аннотации @BeforeAll, @BeforeEach, @AfterEach и @AfterAll.
@RepeatedTest(5)
void repeatedTest() {
System.out.println("Этот тест будет запущен пять раз. ");
}
Стоит отметить, что можно настроить дополнительный вывод информации о запусках теста. Например, показывать номер запуска. За это отвечают специальные константы определенные внутри этой же аннотации.
2.9. Параметризированные тесты
Параметризированные тесты позволяют запускать тест несколько раз с различными входными данными. На данный момент поддерживаются только данные примитивных типов: int, long, double, String. Но не стоит отчаиваться! JUnit 5 определяет несколько дополнительных аннотаций для указания источника данных для параметризированных тестов. Итак, начнём!
@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void testWithStringParameter(String argument) {
assertNotNull(argument);
}
Еще один вдохновляющий пример с @ValueSource.
@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate date) {
assertEquals(2017, date.getYear());
}
Пример с разбором CSV.
@ParameterizedTest
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
// или даже так: @CsvFileSource(resources = "/two-column.csv")
void testWithCsvSource(String first, int second) {
assertNotNull(first);
assertNotEquals(0, second);
}
Пример с Enum.
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(TimeUnit timeUnit) {
assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}
Пример с источником данных.
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
static class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("foo", "bar").map(Arguments::of);
}
}
Еще больше крутых примеров можно найти на официальном сайте в разделе 3.13. Parameterized Tests.
2.10. Аннотированные default методы в интерфейсах
JUnit теперь умеет работать с default методами в интерфейсах! Вот один из официальных примеров применения этого нововведения. Предлагаю посмотреть интересный пример с Equals Contract.
public interface Testable<T> {
T createValue();
}
public interface EqualsContract<T> extends Testable<T> {
T createNotEqualValue();
@Test
default void valueEqualsItself() {
T value = createValue();
assertEquals(value, value);
}
@Test
default void valueDoesNotEqualNull() {
T value = createValue();
assertFalse(value.equals(null));
}
@Test
default void valueDoesNotEqualDifferentValue() {
T value = createValue();
T differentValue = createNotEqualValue();
assertNotEquals(value, differentValue);
assertNotEquals(differentValue, value);
}
}
Заключение
Очень здорово, что популярный фреймворк для тестирования решается на такие серьезные эксперименты с API и старается идти в ногу со временем!
На последок оставлю парочку ссылок: официальный сайт JUnit 5 и очень дружелюбное руководство.
Еще много чего интересного осталось за рамками этой статьи. Например, отдельного обзора заслуживает механизм расширений, предоставляемый JUnit 5.
Спасибо за внимание!
Happy coding!
Автор: atygaev