10 интересных нововведений в JUnit 5 +20


В минувшее воскресенье 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!




К сожалению, не доступен сервер mySQL