Spring Boot предусматривает ряд утилит и аннотаций, упрощающих тестирование приложения. Поддержка тестирования обеспечивается двумя модулями: spring-boot-test содержит основные элементы, а spring-boot-test-autoconfigure поддерживает автоконфигурацию для тестов.

Большинство разработчиков используют "стартер" spring-boot-starter-test, который импортирует оба тестовых модуля Spring Boot, а также JUnit Jupiter, AssertJ, Hamcrest и ряд других полезных библиотек.

Если у вас есть тесты, использующие JUnit 4, для их запуска можно использовать винтажный движок JUnit 5. Чтобы использовать винтажный движок, добавьте зависимость от junit-vintage-engine, как показано в следующем примере:

<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

hamcrest-core исключен в пользу org.hamcrest:hamcrest, который является частью spring-boot-starter-test.

Зависимости тестовой области доступности

"стартер" spring-boot-starter-testtest scope) содержит следующие предусмотренные библиотеки:

  • JUnit 5: Стандарт де-факто для модульного тестирования Java-приложений.

  • Spring Test и Spring Boot Test: Средства поддержки утилит и интеграционных тестов для приложений Spring Boot.

  • AssertJ: Библиотека текучих утверждений.

  • Hamcrest: Библиотека объектов-сопоставителей (matchers) (также известных как ограничения или предикаты).

  • Mockito: Java-фреймворк для имитаций (мокирования)

  • JSONassert: Библиотека утверждений для JSON.

  • JsonPath: XPath для JSON.

Обычно мы находим эти общие библиотеки полезными при написании тестов. Если эти библиотеки не удовлетворяют вашим потребностям, то можно добавить дополнительные тестовые зависимости по своему усмотрению.

Тестирование приложений Spring

Одно из главных преимуществ внедрения зависимостей заключается в том, что оно должно облегчить модульное тестирование кода. Можно создавать экземпляры объектов с помощью оператора new, даже не вовлекая Spring. Также можно использовать объекты-имитациивместо реальных зависимостей.

Часто требуется выйти за рамки модульного тестирования и начать интеграционное тестирование (с ApplicationContext из Spring). Полезно иметь возможность проводить интеграционное тестирование без обязательного развертывания приложения или подключения к другой инфраструктуре.

Spring Framework содержит специальный тестовый модуль для такого интеграционного тестирования. Можно объявить зависимость непосредственно от org.springframework:spring-test или использовать "стартер" spring-boot-starter-test для транзитивного подключения.

Тестирование приложений Spring Boot

Приложение Spring Boot – это ApplicationContext для Spring, поэтому для его тестирования не требуется ничего особенного, кроме тех операций, которые выполняются для ванильного контекста Spring.

Внешние свойства, журналирование и другие функции Spring Boot устанавливаются в контекст по умолчанию, только если вы используете SpringApplication для его создания.

Spring Boot предусматривает аннотацию @SpringBootTest, которую можно использовать в качестве альтернативы стандартной аннотации @ContextConfiguration для spring-test, когда возникает потребность в функциях Spring Boot. Аннотация создает используемый в тестах ApplicationContext через SpringApplication. В дополнение к аннотации @SpringBootTest также предусмотрен ряд других аннотаций для тестирования более конкретных срезов приложения.

Если вы используете JUnit 4, не забудьте также добавить @RunWith(SpringRunner.class) к тесту, иначе аннотации будут проигнорированы. Если вы используете JUnit 5, нет необходимости добавлять эквивалент аннотации @ExtendWith(SpringExtension.class), поскольку аннотация @SpringBootTest и другие аннотации @…​Test уже аннотированы им.

По умолчанию аннотация @SpringBootTest не запускает сервер. Вы можете использовать атрибут webEnvironment для аннотации @SpringBootTest для дальнейшего уточнения хода выполнения ваших тестов:

  • MOCK(по умолчанию) : загружает веб-контекст ApplicationContext и предоставляет объект-имитацию веб-окружения. Встроенные серверы не запускаются при использовании этой аннотации. Если веб-окружение недоступно в вашем classpath, этот режим прозрачно возвращается к созданию обычного не-веб ApplicationContext. Его можно использовать в сочетании с аннотацией @AutoConfigureMockMvc или @AutoConfigureWebTestClient для тестирования веб-приложения на основе имитации.

  • RANDOM_PORT: загружает WebServerApplicationContext и обеспечивает реальное веб-окружение. Встроенные серверы запускаются и прослушивают произвольный порт.

  • DEFINED_PORT: загружает WebServerApplicationContext и обеспечивает реальное веб-окружение. Встроенные серверы запускаются и прослушивают заданный порт (из файла application.properties) или порт по умолчанию 8080.

  • NONE: загружает ApplicationContext с помощью SpringApplication, но не предоставляет никакого веб-окружения (имитационного или иного).

Если ваш тест помечен аннотацией @Transactional, он по умолчанию откатывает транзакцию в конце каждого тестового метода. Однако, поскольку использование этой схемы с RANDOM_PORT или DEFINED_PORT неявным образом предоставляет реальное окружение сервлетов, HTTP-клиент и сервер работают в отдельных потоках и, таким образом, в отдельных транзакциях. Любая транзакция, инициированная на сервере, в этом случае не откатывается.
Аннотация @SpringBootTest с webEnvironment = WebEnvironment.RANDOM_PORT также запускает сервер управления через отдельный случайный порт, если приложение использует иной порт для сервера управления.

Определение типа веб-приложения

Если Spring MVC доступен, конфигурируется обычный контекст приложения на основе MVC. Если имеется только Spring WebFlux, это будет определено, а контекст приложения будет сконфигурирован на основе WebFlux.

Если присутствуют оба фреймворка, приоритет отдается Spring MVC. Если необходимо протестировать реактивное веб-приложение в этом сценарии, требуется установить свойство spring.main.web-application-type:

Java
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(properties = "spring.main.web-application-type=reactive")
class MyWebFluxTests {
    // ...
}
Kotlin
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest(properties = ["spring.main.web-application-type=reactive"])
class MyWebFluxTests {
    // ...
}

Определение тестовой конфигурации

Если вы знакомы с Spring Test Framework, то, вероятно, привыкли использовать @ContextConfiguration(classes=…​), чтобы задавать, какую @Configuration для Spring следует загрузить. Как вариант, вы могли часто использовать вложенные классы с аннотацией @Configuration в своем тесте.

При тестировании приложений Spring Boot это обычно не требуется. Аннотации @*Test в Spring Boot осуществляют поиск вашей первичной конфигурации автоматически, если она не была определена вами явно.

Алгоритм поиска начинает работу с пакета, содержащего тест, пока не найдет класс, аннотированный @SpringBootApplication или @SpringBootConfiguration. При условии, что вы структурировали свой код адекватным образом, основную конфигурацию обычно удается найти.

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

Базовая конфигурация сканирования компонентов, включающая аннотацию @SpringBootApplication, определяет фильтры исключения, которые используются для того, чтобы убедиться, что получение срезов работает так, как предполагается. Если используется явная директива с аннотацией @ComponentScan для помеченного аннотацией @SpringBootApplication класса, имейте в виду, что данные фильтры будут отключены. Если вы прибегаете к получению срезов, то следует определить их еще раз.

Если нужно настроить первичную конфигурацию, то можно использовать вложенный класс, помеченный аннотацией @TestConfiguration. В отличие от вложенного класса, помеченного аннотацией @Configuration, который используется вместо основной конфигурации приложения, вложенный класс с аннотацией @TestConfiguration используется в дополнение к основной конфигурации приложения.

Тестовый фреймворк Spring кэширует контексты приложений между тестами. Поэтому, пока тесты используют одну и ту же конфигурацию (независимо от того, каким образом она была обнаружена), потенциально ресурсоёмкий процесс загрузки контекста происходит лишь единожды.

Исключение тестовой конфигурации

Если приложение использует сканирование компонентов (например, при использовании аннотаций @SpringBootApplication или @ComponentScan), можно обнаружить, что высокоуровневые конфигурационные классы, которые были созданы только для определенных тестов, случайно перехватываются повсюду.

Аннотацию @TestConfiguration можно использовать во внутреннем тестовом классе для настройки первичной конфигурации. Если аннотацией @TestConfiguration помечает высокоуровневый класс, то она указывает, что классы в src/test/java не должны перехватываться при сканировании. Затем можно импортировать этот класс в явном виде, когда это необходимо, как показано в следующем примере:

Java
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
@SpringBootTest
@Import(MyTestsConfiguration.class)
class MyTests {
    @Test
    void exampleTest() {
        // ...
    }
}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import
@SpringBootTest
@Import(MyTestsConfiguration::class)
class MyTests {
    @Test
    fun exampleTest() {
        // ...
    }
}
Если аннотация @ComponentScan используется напрямую (то есть не через аннотацию @SpringBootApplication), необходимо зарегистрировать TypeExcludeFilter вместе с ней.

Использование аргументов приложения

Если приложение принимает аргументы, то можно с помощью аннотации @SpringBootTest внедрить их через атрибут args.

Java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(args = "--app.test=one")
class MyApplicationArgumentTests {
    @Test
    void applicationArgumentsPopulated(@Autowired ApplicationArguments args) {
        assertThat(args.getOptionNames()).containsOnly("app.test");
        assertThat(args.getOptionValues("app.test")).containsOnly("one");
    }
}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest(args = ["--app.test=one"])
class MyApplicationArgumentTests {
    @Test
    fun applicationArgumentsPopulated(@Autowired args: ApplicationArguments) {
        assertThat(args.optionNames).containsOnly("app.test")
        assertThat(args.getOptionValues("app.test")).containsOnly("one")
    }
}

Тестирование с помощью имитационного окружения

По умолчанию аннотация @SpringBootTest не запускает сервер, а вместо этого создает имитационное окружение для тестирования конечных веб-точек.

В Spring MVC можно запрашивать конечные веб-точки с помощью MockMvc или WebTestClient, как показано в следующем примере:

Java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class MyMockMvcTests {
    @Test
    void testWithMockMvc(@Autowired MockMvc mvc) throws Exception {
        mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World"));
    }
    // Если Spring WebFlux находится в classpath, то можно управлять MVC-тестами с помощью WebTestClient
    @Test
    void testWithWebTestClient(@Autowired WebTestClient webClient) {
        webClient
                .get().uri("/")
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("Hello World");
    }
}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
@SpringBootTest
@AutoConfigureMockMvc
class MyMockMvcTests {
    @Test
    fun testWithMockMvc(@Autowired mvc: MockMvc) {
        mvc.perform(MockMvcRequestBuilders.get("/")).andExpect(MockMvcResultMatchers.status().isOk)
            .andExpect(MockMvcResultMatchers.content().string("Hello World"))
    }
    // Если Spring WebFlux находится в classpath, то можно управлять MVC-тестами с помощью WebTestClient
    @Test
    fun testWithWebTestClient(@Autowired webClient: WebTestClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk
            .expectBody<String>().isEqualTo("Hello World")
    }
}
Если необходимо сосредоточиться только на веб-уровне и не запускать полный ApplicationContext, то в качестве альтернативы рассмотрите вариант использования аннотации @WebMvcTest.

Можно использовать WebTestClient с конечными точками Spring WebFlux так, как показано в следующем примере:

Java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;
@SpringBootTest
@AutoConfigureWebTestClient
class MyMockWebTestClientTests {
    @Test
    void exampleTest(@Autowired WebTestClient webClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk()
            .expectBody(String.class).isEqualTo("Hello World");
    }
}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@SpringBootTest
@AutoConfigureWebTestClient
class MyMockWebTestClientTests {
    @Test
    fun exampleTest(@Autowired webClient: WebTestClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk
            .expectBody<String>().isEqualTo("Hello World")
    }
}

Тестирование в имитационном окружении обычно проходит быстрее, чем при использовании заполненного контейнера сервлетов. Однако, поскольку имитирование происходит на уровне Spring MVC, код, который опирается на логику работы контейнера сервлетов более низкого уровня, не может быть непосредственно протестирован с помощью MockMvc.

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

Тестирование при помощи работающего сервера

Если требуется запустить полноценно работающий сервер, то рекомендуем использовать случайные порты. Если вы используете @SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT), доступный порт выбирается случайным образом при каждом запуске теста.

Аннотацию @LocalServerPort можно использоваться для того, чтобы внедрить фактически используемый порт в тест. Для удобства тесты, которым необходимо выполнять REST-вызовы к запущенному серверу, могут дополнительно через аннотацию @Autowire привязывать WebTestClient, который разрешает относительные ссылки на запущенный сервер и оснащен специальным APIом для проверки ответов, как показано в следующем примере:

Java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.web.reactive.server.WebTestClient;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortWebTestClientTests {
    @Test
    void exampleTest(@Autowired WebTestClient webClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk()
            .expectBody(String.class).isEqualTo("Hello World");
    }
}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortWebTestClientTests {
    @Test
    fun exampleTest(@Autowired webClient: WebTestClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk
            .expectBody<String>().isEqualTo("Hello World")
    }
}
WebTestClient можно использовать как для реальных серверов, так и для имитационных окружений.

Эта конфигурация требует наличия spring-webflux в classpath. Если вы не можете или не хотите добавлять webflux, Spring Boot также предусматривает возможность использования TestRestTemplate:

Java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortTestRestTemplateTests {
    @Test
    void exampleTest(@Autowired TestRestTemplate restTemplate) {
        String body = restTemplate.getForObject("/", String.class);
        assertThat(body).isEqualTo("Hello World");
    }
}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.boot.test.web.client.TestRestTemplate
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortTestRestTemplateTests {
    @Test
    fun exampleTest(@Autowired restTemplate: TestRestTemplate) {
        val body = restTemplate.getForObject("/", String::class.java)
        assertThat(body).isEqualTo("Hello World")
    }
}

Настройка WebTestClient

Чтобы настроить бин WebTestClient, сконфигурируйте бин WebTestClientBuilderCustomizer. Любые подобные бины вызываются с помощью WebTestClient.Builder, который используется для создания WebTestClient.

Использование JMX

Поскольку фреймворк тестового контекста кэширует контекст, технология JMX по умолчанию отключена, чтобы предотвратить регистрацию идентичных компонентов в одном и том же домене. Если такому тесту требуется доступ к MBeanServer, подумайте о том, чтобы пометить его как грязный:

Java
import javax.management.MBeanServer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(properties = "spring.jmx.enabled=true")
@DirtiesContext
class MyJmxTests {
    @Autowired
    private MBeanServer mBeanServer;
    @Test
    void exampleTest() {
        assertThat(this.mBeanServer.getDomains()).contains("java.lang");
        // ...
    }
}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.annotation.DirtiesContext
import javax.management.MBeanServer
@SpringBootTest(properties = ["spring.jmx.enabled=true"])
@DirtiesContext
class MyJmxTests(@Autowired val mBeanServer: MBeanServer) {
    @Test
    fun exampleTest() {
        assertThat(mBeanServer.domains).contains("java.lang")
        // ...
    }
}

Использование метрик

Независимо от classpath, реестры счетчиков, за исключением хранящихся в памяти, не конфигурируются автоматически при использовании аннотации @SpringBootTest.

Если необходимо экспортировать метрики в другой бэкенд в рамках интеграционного теста, аннотируйте его с помощью @AutoConfigureMetrics.