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 не повинні перехоплюватися під час сканування. Потім можна імпортувати цей клас у явному вигляді, коли це необхідно, як показано в наступному прикладі:

block-title">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.