Біни-імітації та біни-шпигуни
При виконанні тестів іноді необхідно імітувати певні компоненти в контексті програми. Наприклад, у тебе може бути фасад певної віддаленої служби, яка недоступна під час розробки. Імітування також може бути корисним, якщо необхідно імітувати збої, які важко викликати в реальному оточенні.
Spring Boot містить анотацію @MockBean, яку можна використовувати для визначення Mockito-імітації для біна всередині ApplicationContext. Можна використовувати анотацію для додавання нових бінів або заміни одного існуючого визначення біну. Анотацію можна використовувати безпосередньо для тестових класів, для полів усередині тесту або для класів та полів з анотацією @Configuration. При використанні з полем екземпляр створеного об'єкта-імітації також впроваджується. Біни-імітації автоматично скидаються після виконання кожного тестового методу.
Якщо тест використовує одну з тестових анотацій Spring Boot (наприклад, @ SpringBootTest), ця функція автоматично активується. Щоб використовувати цю функцію з іншим типом організації, необхідно явно додати слухачі, як показано в наступному прикладі:
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 {
// ...
}
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 замінюється імітаційною реалізацією:
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");
}
}
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.
final. Це зупиняє коректну роботу фреймворку Mockito, оскільки він не може імітувати або стежити за
final методами у своїй конфігурації за замовчуванням. Якщо потрібно імітувати або стежити за таким біном, налаштуй Mockito на використання вбудованого конструктора об'єктів-імітацій, додавши
org.mockito:mockito-inline у тестові залежності програми. Це дозволить Mockito імітувати і стежити за
final методами.
@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з JacksonGsonJsonb
@JsonTest можна знайти в додатку до документації.
Якщо необхідно конфігурувати елементи автоконфігурації, можна використовувати анотацію @AutoConfigureJsonTesters.
Spring Boot містить допоміжні класи на основі AssertJ, які працюють з бібліотеками JSONAssert та JsonPath, призначені для перевірки коректного передбачуваного відображення JSON. Класи JacksonTester, GsonTester, JsonbTester та BasicJsonTester можна використовувати для бібліотек Jackson, Gson, Jsonb та Strings відповідно. Будь-які допоміжні поля тестового класу можуть бути позначені анотацією @Autowired під час використання анотації @JsonTest. У цьому прикладі показаний тестовий клас для бібліотеки Jackson:
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");
}
}
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")
}
}
initFields допоміжного класу у своєму методі, анотованому
@Before, якщо анотація
@JsonTest не використовується.
Якщо допоміжні класи Spring Boot на основі AssertJ використовуються для додання затвердження для значення числа в зазначеному JSON-шляху, можна і не використовувати isEqualTo залежно від типу. Натомість можна використовувати функцію satisfies в AssertJ для додавання затвердження відповідності для значення вказаній умові. Наприклад, в наступному прикладі додано твердження, що фактичне число є плаваючим значенням, близьким до 0.15 в межах зміщення 0.01.
@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)));
}
@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:
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"));
}
}
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:
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");
}
}
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")
}
}
WebDriver у спеціальну "область доступності", щоб забезпечити вихід драйвера після кожного тесту та впровадження нового екземпляра. Якщо така логіка роботи зайва, можна додати анотацію
@Scope("singleton") на визначення твого
WebDriver з анотацією
@Bean.
webDriver, створена Spring Boot, замінить будь-яку визначену користувачем область доступності з тим же ім'ям. Якщо визначено власну область доступності
webDriver, то можна виявити, що вона перестає працювати при використанні анотації
@WebMvcTest.
Якщо в classpath є Spring Security, анотація @WebMvcTest також скануватиме біни WebSecurityConfigurer. Замість того, щоб повністю відключати засоби безпеки для таких тестів, можна використовувати засоби підтримки тестів з Spring Security.
Автоконфігуровані тести 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:
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");
}
}
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")
}
}
WebTestClient в імітованому вебдодатку наразі працює тільки у WebFlux.
@WebFluxTest не може виявляти маршрути, зареєстровані за допомогою функціонального вебфреймворку. Для тестування бінів
RouterFunction у контексті розглянь можливість самостійного імпорту
RouterFunction за допомогою анотації
@Import або за допомогою анотації
@SpringBootTest.
@WebFluxTest не може виявляти кастомну конфігурацію безпеки, зареєстровану як
@Bean типу
SecurityWebFilterChain. Щоб включити її до свого тесту, потрібно імпортувати конфігурацію, що реєструє бін, за допомогою анотації
@Import або анотації
@SpringBootTest.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ