Бины-имитации и бины-шпионы

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

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 может помочь запустить полнофункциональные сквозные тесты с использованием реального сервера.