Рубрики
IT Java test-driven unit-test Разработка ПО

Введение в JUnit 5

Знакомство с основными концепциями фреймворка JUnit 5, подключение и базовое использование. Как писать и запускать @Test

Знакомство с основными концепциями фреймворка тестирования 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.

Этой информации достаточно для уверенного старта написания ваших собственных тестов. Напоминаю, что подробнее о возможностях фреймворка можно ознакомиться в официальной документации.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *