Біни-імітації та біни-шпигуни

При виконанні тестів іноді необхідно імітувати певні компоненти в контексті програми. Наприклад, у тебе може бути фасад певної віддаленої служби, яка недоступна під час розробки. Імітування також може бути корисним, якщо необхідно імітувати збої, які важко викликати в реальному оточенні.

Spring Boot містить анотацію @MockBean, яку можна використовувати для визначення Mockito-імітації для біна всередині ApplicationContext. Можна використовувати анотацію для додавання нових бінів або заміни одного існуючого визначення біну. Анотацію можна використовувати безпосередньо для тестових класів, для полів усередині тесту або для класів та полів з анотацією @Configuration. При використанні з полем екземпляр створеного об'єкта-імітації також впроваджується. Біни-імітації автоматично скидаються після виконання кожного тестового методу.

Якщо тест використовує одну з тестових анотацій Spring Boot (наприклад, @ SpringBootTest), ця функція автоматично активується. Щоб використовувати цю функцію з іншим типом організації, необхідно явно додати слухачі, як показано в наступному прикладі:

Java

import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener;
import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
@ContextConfiguration(classes = MyConfig.class)
@TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class })
class MyTests {
    // ...
}
Kotlin

import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener
import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestExecutionListeners
@ContextConfiguration(classes = [MyConfig::class])
@TestExecutionListeners(
    MockitoTestExecutionListener::class,
    ResetMocksTestExecutionListener::class
)
class MyTests {
    // ...
}

У наступному прикладі існуючий бін RemoteService замінюється імітаційною реалізацією:

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.mock.mockito.MockBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@SpringBootTest
class MyTests {
    @Autowired
    private Reverser reverser;
    @MockBean
    private RemoteService remoteService;
    @Test
    void exampleTest() {
        given(this.remoteService.getValue()).willReturn("spring");
        String reverse = this.reverser.getReverseValue(); // Calls injected RemoteService
        assertThat(reverse).isEqualTo("gnirps");
    }
}
Kotlin

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
@SpringBootTest
class MyTests(@Autowired val reverser: Reverser, @MockBean val remoteService: RemoteService) {
    @Test
    fun exampleTest() {
        given(remoteService.value).willReturn("spring")
        val reverse = reverser.reverseValue // Calls injected RemoteService
        assertThat(reverse).isEqualTo("gnirps")
    }
}
-Анотацію @MockBean не можна використовувати для імітування логіки роботи біна, який виконується під час оновлення контексту програми. До моменту виконання тесту оновлення контексту програми вже буде завершено, а налаштовувати логіку роботи, що імітується, буде вже пізно. У цій ситуації ми рекомендуємо використовувати метод, позначений анотацією @Bean, для створення та конфігурування об'єкта-імітації.

До того ж, можна використовувати анотацію @SpyBean, щоб обернути будь-який існуючий бін об'єктом spy з Mockito.

Проксі з CGLib, наприклад, створені для бінів, що входять до області доступності, оголошують методи, що проксуються, як final. Це зупиняє коректну роботу фреймворку Mockito, оскільки він не може імітувати або стежити за final методами у своїй конфігурації за замовчуванням. Якщо потрібно імітувати або стежити за таким біном, налаштуй Mockito на використання вбудованого конструктора об'єктів-імітацій, додавши org.mockito:mockito-inline у тестові залежності програми. Це дозволить Mockito імітувати і стежити за final методами.
Хоча тестовий фреймворк Spring кешує контексти додатків між тестами і повторно використовує контекст для тестів з однаковою конфігурацією використання анотацій @MockBean або @SpyBean впливає на ключ кеша, що, швидше за все, збільшить кількість контекстів.
Якщо ти використовуєш анотацію @SpyBean, щоб стежити за бінами з методами, анотованими @Cacheable, які посилаються на параметри на ім'я, твоя програма має бути скомпільована з параметром -parameters. Таким чином, інфраструктура кешування гарантовано отримає доступ до імен параметрів після того, як бін буде виявлено. Якщо анотація @SpyBean використовується для стеження за проксованим у Spring біном, в деяких ситуаціях може знадобитися видалити Spring-проксі, наприклад, при встановленні очікуваних подій за допомогою given або when. Для цього використовується AopTestUtils.getTargetObject(yourProxiedSpy).

Автоконфігуровані тести

Система автоконфігурації в Spring Boot відмінно працює з додатками, але іноді може бути надмірною для тестів. Найчастіше практично завантажувати ті частини конфігурації, які необхідні тестування " шару" докладання. Наприклад, може виникнути необхідність переконатися, що контролери Spring MVC коректно відображають URL-адреси, але при цьому не потрібно залучати виклики бази даних до цих тестів, або може знадобитися протестувати JPA-сутності, але вебрівень при виконанні цих тестів не становить інтересу.

Модуль spring-boot-test-autoconfigure містить низку анотацій, які можна використовувати для автоматичної конфігурації таких "шарів". Кожна з них працює однаково, являючи собою анотацію виду @… Test, яка завантажує ApplicationContext, і одну або кілька анотацій виду @AutoConfigure…, які можна використовувати для налаштування параметрів автоконфігурації.

Кожен шар обмежує сканування компонентів відповідними компонентами та завантажує дуже обмежений набір автоконфігураційних класів. Якщо необхідно виключити один з них, більшість анотацій виду @… Test передбачають атрибут excludeAutoConfiguration. В якості альтернативи можна використовувати анотацію @ImportAutoConfiguration#exclude.
Увімкнення кількох "шарів" за допомогою декількох анотацій виду @… Test в один тест не підтримуються. Якщо потрібно кілька "шарів", виберіть одну з анотацій виду @… Test і додай анотації виду @AutoConfigure… інших "шарів" вручну.
Також можна використовувати анотації виду @AutoConfigure… зі стандартною анотацією @SpringBootTest. Можна використовувати цю комбінацію, якщо тебе не цікавить створення шарів твоєї програми, але тобі потрібні деякі з автоматично конфігурованих тестових бінів.

Автоконфігуровані тести JSON

Щоб переконатися, що серіалізація та десеріалізація JSON-об'єктів працює передбачуваним чином, можна використовувати анотацію @JsonTest. Анотація @JsonTest автоматично конфігурує доступний підтримуваний JSON-сумісник, який може бути однією з таких бібліотек:

  • ObjectMapper з Jackson, будь-які біни з анотацією @JsonComponent та будь-які Modules з Jackson

  • Gson

  • Jsonb

Список автоконфігурацій, активованих анотацією @JsonTest можна знайти в додатку до документації.

Якщо необхідно конфігурувати елементи автоконфігурації, можна використовувати анотацію @AutoConfigureJsonTesters.

Spring Boot містить допоміжні класи на основі AssertJ, які працюють з бібліотеками JSONAssert та JsonPath, призначені для перевірки коректного передбачуваного відображення JSON. Класи JacksonTester, GsonTester, JsonbTester та BasicJsonTester можна використовувати для бібліотек Jackson, Gson, Jsonb та Strings відповідно. Будь-які допоміжні поля тестового класу можуть бути позначені анотацією @Autowired під час використання анотації @JsonTest. У цьому прикладі показаний тестовий клас для бібліотеки Jackson:

Java

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest
class MyJsonTests {
    @Autowired
    private JacksonTester<VehicleDetails> json;
    @Test
    void serialize() throws Exception {
        VehicleDetails details = new VehicleDetails("Honda", "Civic");
        // Твердження для файлу ".json" у тому ж пакеті, що і тест
        assertThat(this.json.write(details)).isEqualToJson("expected.json");
        // Або використовуємо твердження на основі JSON-шляху
        assertThat(this.json.write(details)).hasJsonPathStringValue("@.make");
        assertThat(this.json.write(details)).extractingJsonPathStringValue("@.make").isEqualTo("Honda");
    }
    @Test
    void deserialize() throws Exception {
        String content = "{\"make\":\"Ford\",\"model\":\"Focus\"}";
        assertThat(this.json.parse(content)).isEqualTo(new VehicleDetails("Ford", "Focus"));
        assertThat(this.json.parseObject(content).getMake()).isEqualTo("Ford");
    }
}
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.autoconfigure.json.JsonTest
import org.springframework.boot.test.json.JacksonTester
@JsonTest
class MyJsonTests(@Autowired val json: JacksonTester<VehicleDetails>) {
    @Test
    fun serialize() {
        val details = VehicleDetails("Honda", "Civic")
        // Затвердження для файлу ".json" у тому ж пакеті, що і тест
        assertThat(json.write(details)).isEqualToJson("expected.json")
        // Або використовуємо затвердження на основі JSON-шляху
        assertThat(json.write(details)).hasJsonPathStringValue("@.make")
        assertThat(json.write(details)).extractingJsonPathStringValue("@.make").isEqualTo("Honda")
    }
    @Test
    fun deserialize() {
        val content = "{\"make\":\"Ford\",\"model\":\"Focus\"}"
        assertThat(json.parse(content)).isEqualTo(VehicleDetails("Ford", "Focus"))
        assertThat(json.parseObject(content).make).isEqualTo("Ford")
    }
}
Допоміжні класи JSON також можна використовувати безпосередньо у стандартних модульних тестах. Для цього виклич метод initFields допоміжного класу у своєму методі, анотованому @Before, якщо анотація @JsonTest не використовується.

Якщо допоміжні класи Spring Boot на основі AssertJ використовуються для додання затвердження для значення числа в зазначеному JSON-шляху, можна і не використовувати isEqualTo залежно від типу. Натомість можна використовувати функцію satisfies в AssertJ для додавання затвердження відповідності для значення вказаній умові. Наприклад, в наступному прикладі додано твердження, що фактичне число є плаваючим значенням, близьким до 0.15 в межах зміщення 0.01.

Java

@Test
void someTest() throws Exception {
    SomeObject value = new SomeObject(0.152f);
    assertThat(this.json.write(value)).extractingJsonPathNumberValue("@.test.numberValue")
            .satisfies((number) -> assertThat(number.floatValue()).isCloseTo(0.15f, within(0.01f)));
}
Kotlin

@Test
fun someTest() {
    val value = SomeObject(0.152f)
    assertThat(json.write(value)).extractingJsonPathNumberValue("@.test.numberValue")
        .satisfies(ThrowingConsumer { number ->
            assertThat(number.toFloat()).isCloseTo(0.15f, within(0.01f))
        })
}

Автоконфігуровані тести Spring MVC

Щоб протестувати контролери Spring MVC на предмет передбаченої працездатності, використовуй анотацію @WebMvcTest. Анотація @WebMvcTest автоматично конфігурує інфраструктуру Spring MVC і обмежує скановані біни до @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations та HandlerMethodArgumentResolver. Звичайні біни з анотаціями @Component та @ConfigurationProperties не підлягають скануванню під час використання анотації @WebMvcTest. Для додавання бінів, позначених анотацією @ConfigurationProperties, можна використовувати анотацію @EnableConfigurationProperties.

Якщо необхідно зареєструвати додаткові компоненти, наприклад, Module з Jackson, можна імпортувати додаткові класи конфігурації, використовуючи анотацію @Import у своєму тесті.

Часто анотація @WebMvcTest обмежується одним контролером і використовується в поєднанні з анотацією @MockBean, щоб передавати імітаційні реалізації для необхідних взаємодіючих об'єктів.

@WebMvcTest також автоматично конфігурує MockMvc. Mock MVC передбачає ефективний спосіб швидкого тестування контролерів MVC без необхідності запуску повноцінного HTTP-сервера. Ти також можеш автоматично налаштувати MockMvc без анотації @WebMvcTest (а наприклад, з анотацією @SpringBootTest), анотувавши його за допомогою @AutoConfigureMockMvc. У цьому прикладі використовується MockMvc:

Java

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
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;
@WebMvcTest(UserVehicleController.class)
class MyControllerTests {
    @Autowired
    private MockMvc mvc;
    @MockBean
    private UserVehicleService userVehicleService;
    @Test
    void testExample() throws Exception {
        given(this.userVehicleService.getVehicleDetails("sboot"))
            .willReturn(new VehicleDetails("Honda", "Civic"));
        this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
            .andExpect(status().isOk())
            .andExpect(content().string("Honda Civic"));
    }
}
Kotlin

import org.junit.jupiter.api.Test
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
@WebMvcTest(UserVehicleController::class)
class MyControllerTests(@Autowired val mvc: MockMvc) {
    @MockBean
    lateinit var userVehicleService: UserVehicleService
    @Test
    fun testExample() {
        given(userVehicleService.getVehicleDetails("sboot"))
            .willReturn(VehicleDetails("Honda", "Civic"))
        mvc.perform(MockMvcRequestBuilders.get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
            .andExpect(MockMvcResultMatchers.status().isOk)
            .andExpect(MockMvcResultMatchers.content().string("Honda Civic"))
    }
}
Якщо необхідно конфігурувати елементи автоконфігурації (наприклад, якщо потрібно застосувати фільтри сервлетів), можна використовувати атрибути в анотації @AutoConfigureMockMvc.

Якщо ти використовуєш HtmlUnit та Selenium, автоконфігурація також передбачає бін WebClient для HtmlUnit та/або бін WebDriver для Selenium. У цьому прикладі використовується HtmlUnit:

Java

import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@WebMvcTest(UserVehicleController.class)
class MyHtmlUnitTests {
    @Autowired
    private WebClient webClient;
    @MockBean
    private UserVehicleService userVehicleService;
    @Test
    void testExample() throws Exception {
        given(this.userVehicleService.getVehicleDetails("sboot")).willReturn(new VehicleDetails("Honda", "Civic"));
        HtmlPage page = this.webClient.getPage("/sboot/vehicle.html");
        assertThat(page.getBody().getTextContent()).isEqualTo("Honda Civic");
    }
}
Kotlin

import com.gargoylesoftware.htmlunit.WebClient
import com.gargoylesoftware.htmlunit.html.HtmlPage
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
@WebMvcTest(UserVehicleController::class)
class MyHtmlUnitTests(@Autowired val webClient: WebClient) {
    @MockBean
    lateinit var userVehicleService: UserVehicleService
    @Test
    fun testExample() {
        given(userVehicleService.getVehicleDetails("sboot")).willReturn(VehicleDetails("Honda", "Civic"))
        val page = webClient.getPage<HtmlPage>("/sboot/vehicle.html")
        assertThat(page.body.textContent).isEqualTo("Honda Civic")
    }
}
За замовчуванням Spring Boots розміщує біни WebDriver у спеціальну "область доступності", щоб забезпечити вихід драйвера після кожного тесту та впровадження нового екземпляра. Якщо така логіка роботи зайва, можна додати анотацію @Scope("singleton") на визначення твого WebDriver з анотацією @Bean.
Область доступності webDriver, створена Spring Boot, замінить будь-яку визначену користувачем область доступності з тим же ім'ям. Якщо визначено власну область доступності webDriver, то можна виявити, що вона перестає працювати при використанні анотації @WebMvcTest.

Якщо в classpath є Spring Security, анотація @WebMvcTest також скануватиме біни WebSecurityConfigurer. Замість того, щоб повністю відключати засоби безпеки для таких тестів, можна використовувати засоби підтримки тестів з Spring Security. Іноді одного написання тестів у Spring MVC недостатньо; Spring Boot допоможе запустити повнофункціональні наскрізні тести з використанням реального сервера.

Автоконфігуровані тести Spring WebFlux

Щоб переконатися, що контролери Spring WebFlux працюють так, як передбачається, можна використовувати анотацію @WebFluxTest. Анотація @WebFluxTest автоматично конфігурує інфраструктуру Spring WebFlux і обмежує скановані біни до @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, WebFilter та WebFluxConfigurer. Звичайні біни з анотаціями @Component та @ConfigurationProperties не підлягають скануванню під час використання анотації @WebFluxTest. Для додавання бінів, позначених анотацією @ConfigurationProperties, можна використовувати анотацію @EnableConfigurationProperties.

Якщо необхідно зареєструвати додаткові компоненти, такі як Module з бібліотеки Jackson, можна імпортувати додаткові конфігураційні класи, використовуючи анотацію @Import у тесті.

Часто анотація @WebFluxTest обмежується одним контролером і використовується в поєднанні з анотацією @MockBean, щоб передавати імітаційні реалізації для необхідних взаємодіючих об'єктів.

Анотація @WebFluxTest також автоматично конфігурує WebTestClient, який передбачає ефективний засіб швидкого тестування контролерів WebFlux без необхідності запуску повноцінного HTTP-сервера.

Ти також можеш автоматично конфігурувати WebTestClient без анотації @WebFluxTest (а наприклад, з анотацією @SpringBootTest), анотувавши його за допомогою @AutoConfigureWebTestClient . У наступному прикладі показаний клас, який використовує анотацію @WebFluxTest та WebTestClient:
Java

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.mockito.BDDMockito.given;
@WebFluxTest(UserVehicleController.class)
class MyControllerTests {
    @Autowired
    private WebTestClient webClient;
    @MockBean
    private UserVehicleService userVehicleService;
    @Test
    void testExample() {
        given(this.userVehicleService.getVehicleDetails("sboot"))
            .willReturn(new VehicleDetails("Honda", "Civic"));
        this.webClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN).exchange()
            .expectStatus().isOk()
            .expectBody(String.class).isEqualTo("Honda Civic");
    }
}
Kotlin

import org.junit.jupiter.api.Test
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@WebFluxTest(UserVehicleController::class)
class MyControllerTests(@Autowired val webClient: WebTestClient) {
    @MockBean
    lateinit var userVehicleService: UserVehicleService
    @Test
    fun testExample() {
        given(userVehicleService.getVehicleDetails("sboot"))
            .willReturn(VehicleDetails("Honda", "Civic"))
        webClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN).exchange()
            .expectStatus().isOk
            .expectBody<String>().isEqualTo("Honda Civic")
    }
}
Ця конфігурація підтримується лише програмами WebFlux, оскільки використання WebTestClient в імітованому вебдодатку наразі працює тільки у WebFlux.
Анотація @WebFluxTest не може виявляти маршрути, зареєстровані за допомогою функціонального вебфреймворку. Для тестування бінів RouterFunction у контексті розглянь можливість самостійного імпорту RouterFunction за допомогою анотації @Import або за допомогою анотації @SpringBootTest.
Анотація @WebFluxTest не може виявляти кастомну конфігурацію безпеки, зареєстровану як @Bean типу SecurityWebFilterChain. Щоб включити її до свого тесту, потрібно імпортувати конфігурацію, що реєструє бін, за допомогою анотації @Import або анотації @SpringBootTest.

Іноді одного написання тестів у Spring WebFlux недостатньо; Spring Boot допоможе запустити повнофункціональні наскрізні тести з використанням реального сервера.