testClient.get().uri("/design/recent")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBodyList(Taco.class)
.contains(Arrays.copyOf(tacos, 12));
Здесь вы утверждаете, что тело ответа содержит список, содержащий те же элементы, что и первые 12 элементов исходного массива Taco, созданного вами в начале тестового метода.
11.3.2 Тестирование POST-запросов
WebTestClient может делать больше, чем просто проверять GET запросы в контроллерах. Его также можно использовать для тестирования любого метода HTTP, включая запросы GET, POST, PUT, PATCH, DELETE и HEAD. Таблица 11.1 отображает методы HTTP на методы WebTestClient.
Таблица 11.1. WebTestClient тестирует любые запросы к контроллерам Spring WebFlux.
HTTP методы : WebTestClient метод
GET : .get()
POST : .post()
PUT : .put()
PATCH : .patch()
DELETE : .delete()
HEAD : .head()
В качестве примера тестирования еще один метод тестирования HTTP-запроса к контроллеру Spring WebFlux, давайте посмотрим на другой тест с DesignTacoController. На этот раз вы напишете тест конечной точки создания taco вашего API, отправив POST запрос в /design:
@Test
public void shouldSaveATaco() {
TacoRepository tacoRepo = Mockito.mock(
TacoRepository.class); //Устанавливает тестовые данные
Mono
Taco savedTaco = testTaco(null);
savedTaco.setId(1L);
Mono
when(tacoRepo.save(any())).thenReturn(savedTacoMono); //Mocks TacoRepository
WebTestClient testClient = WebTestClient.bindToController( //Создание WebTestClient
new DesignTacoController(tacoRepo)).build();
testClient.post() //POST для тако
.uri("/design")
.contentType(MediaType.APPLICATION_JSON)
.body(unsavedTacoMono, Taco.class)
.exchange()
.expectStatus().isCreated() //Проверяет ответ
.expectBody(Taco.class)
.isEqualTo(savedTaco);
}
Как и в предыдущем методе тестирования, shouldSaveATaco начинается с настройки некоторых тестовых данных, моккинга TacoRepository, и создания WebTestClient, который привязан к контроллеру. Затем он использует WebTestClient для отправки POST запроса в /design с телом типа application/json и полезной нагрузкой, которая является JSON-сериализованной формой Taco в несохраненном Mono. После выполнения exchange() тест утверждает, что ответ имеет статус HTTP 201 (CREATED) и полезную нагрузку в теле, равную сохраненному объекту Taco.
11.3.3. Тестирование на живом сервере
Тесты, которые вы написали до сих пор, основывались на моковской реализации среды Spring WebFlux, так что реальный сервер не понадобился бы. Но вам может потребоваться протестировать контроллер WebFlux в контексте сервера, такого как Netty или Tomcat, и, возможно, с хранилищем или другими зависимостями. То есть вы должны быть способны написать интеграционный тест.
Чтобы написать интеграционный тест WebTestClient, начните с аннотирования класса теста с помощью @RunWith и @SpringBootTest, как и любого другого интеграционного теста Spring Boot:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class DesignTacoControllerWebTest {
@Autowired
private WebTestClient testClient;
}
Установив для атрибута webEnvironment значение WebEnvironment.RANDOM_PORT, вы просите Spring запустить работающий сервер, прослушивающий случайным образом выбранный порт (Вы могли бы также установить для webEnvironment значение WebEnvironment.DEFINED_PORT и указать порт с атрибутом properties, но это обычно нежелательно. Это открывает риск столкновения портов с работающим сервером).
Вы, думаю, заметите, что вы также автоматически подключили WebTestClient к тестовому классу. Это означает не только то, что вам больше не нужно создавать его в своих методах тестирования, но и то, что вам не нужно указывать полный URL-адрес при отправке запросов. Это потому, что WebTestClient будет настроен, чтобы узнать, на каком порту работает тестовый сервер. Теперь вы можете переписать shouldReturnRecentTacos() в качестве интеграционного теста, в котором используется автоматическая привязка WebTestClient:
@Test
public void shouldReturnRecentTacos() throws IOException {
testClient.get().uri("/design/recent")
.accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[?(@.id == 'TACO1')].name")
.isEqualTo("Carnivore")
.jsonPath("$[?(@.id == 'TACO2')].name")
.isEqualTo("Bovine Bounty")
.jsonPath("$[?(@.id == 'TACO3')].name")
.isEqualTo("Veg-Out");
}
Вы, несомненно, заметили, что в этой новой версии shouldReturnRecentTacos() кода гораздо меньше. Больше нет необходимости создавать WebTestClient, потому что вы будете использовать экземпляр с автосвязыванием. И нет необходимости издеваться над TacoRepository, потому что Spring создаст экземпляр DesignTacoController и внедрит его в настоящий TacoRepository. В этой новой версии метода теста вы используете выражения JSONPath для проверки значений, передаваемых из базы данных.
WebTestClient полезен, когда в ходе теста вам нужно использовать API, предоставляемый контроллером WebFlux. Но что делать, когда само ваше приложение использует какой-то другой API? Давайте обратим наше внимание на клиентскую сторону реактивной веб-истории Spring и посмотрим, как WebClient предоставляет REST-клиент, который работает с реактивными типами, такими как Mono и Flux.
11.4 Использование реактивного API REST
В главе 7 вы использовали RestTemplate для отправки клиентских запросов в Taco Cloud API. RestTemplate является старым таймером, появившимся в Spring версии 3.0. В свое время он использовался для бесчисленных запросов от имени приложений, которые его используют.
Но все методы, предоставляемые RestTemplate, работают с нереактивными типами доменов и коллекциями. Это означает, что если вы хотите работать с данными ответа реактивным способом, вам нужно обернуть их в Flux или Mono. И если у вас уже есть Flux или Mono, и вы хотите отправить их в запросе POST или PUT, вам нужно будет извлечь данные в нереактивный тип, прежде чем делать запрос.
Было бы хорошо, если бы был способ использовать RestTemplate изначально с реактивными типами. Не бойтесь. Spring 5 предлагает WebClient в качестве реактивной альтернативы RestTemplate. WebClient позволяет отправлять и получать реактивные типы при отправке запросов на внешние API.
Использование WebClient сильно отличается от использования RestTemplate. Вместо того, чтобы иметь несколько методов для обработки различных типов запросов, WebClient имеет свободный интерфейс в стиле конструктора, который позволяет вам описывать и отправлять запросы. Общий шаблон использования для работы с WebClient:
-Создать экземпляр WebClient (или внедрить bean-компонент WebClient)
-Укажите HTTP метод запроса на отправку
-Укажите URI и любые заголовки, которые должны быть в запросе
-Отправить request
-Получить response
Давайте рассмотрим несколько примеров работы WebClient, начиная с того, как использовать WebClient для отправки HTTP-запросов GET.
11.4.1 GET ресурсов
В качестве примера использования WebClient, предположим, что вам нужно извлечь объект Ingredient по его идентификатору из Taco Cloud API. Используя RestTemplate, вы можете использовать метод getForObject(). Но с WebClient вы создаете запрос, получаете ответ, а затем извлекаете Mono, который публикует объект Ingredient:
Mono
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.retrieve()
.bodyToMono(Ingredient.class);
ingredient.subscribe(i -> { ... })
Здесь вы создаете новый экземпляр WebClient с помощью create(). Затем вы используете get() и uri(), чтобы определить запрос GET для http://localhost:8080/ingredients/{id}, где заполнитель {id} будет заменен значением ingredientId.. Метод retrieve() выполняет запрос. Наконец, вызов bodyToMono() извлекает полезную нагрузку ответа в Mono
Чтобы применить дополнительные операции к Mono, возвращенному из bodyToMono(), важно подписаться на него еще до того, как запрос будет отправлен. Делать запросы, которые могут вернуть коллекцию значений, так же просто. Например, следующий фрагмент кода выбирает все ингредиенты:
Flux
.get()
.uri("http://localhost:8080/ingredients")
.retrieve()
.bodyToFlux(Ingredient.class);
ingredients.subscribe(i -> { ... })
В большинстве случаев выборка нескольких элементов аналогична отправке запроса на один элемент. Большая разница в том, что вместо того, чтобы использовать bodyToMono() для извлечения тела ответа в Mono, вы используете bodyToFlux() для извлечения его во Flux.
Как и в случае с bodyToMono(), Flux, возвращенный из bodyToFlux(), еще не подписан. Это позволяет применять дополнительные операции (фильтры, map-ы и т. д.) к Flux до того, как данные начнут проходить через него. Поэтому важно подписаться на получившийся Flux, иначе запрос даже не будет отправлен.
ВЫПОЛНЕНИЕ ЗАПРОСОВ С БАЗОВЫМ URI
Вы можете использовать общий базовый URI для множества разных запросов. В этом случае может быть полезно создать bean-компонент WebClient с базовым URI и внедрить его везде, где это необходимо. Такой bean может быть объявлен так:
@Bean
public WebClient webClient() {