1. Обзор
В этой статье мы собираемся изучить интеграционное тестирование клиента Feign .
Мы создадим базовый клиент Open Feign, для которого напишем простой интеграционный тест с помощью WireMock .
После этого мы добавим конфигурацию ленты в наш клиент, а также создадим для нее интеграционный тест. И, наконец, мы настроим тестовый контейнер Eureka и протестируем эту настройку , чтобы убедиться, что вся наша конфигурация работает должным образом. ** ** ****
2. Притворный клиент
Чтобы настроить наш клиент Feign, мы должны сначала добавить зависимость Spring Cloud OpenFeign Maven:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
После этого создадим класс Book
для нашей модели:
public class Book {
private String title;
private String author;
}
И, наконец, давайте создадим наш интерфейс Feign Client:
@FeignClient(value="simple-books-client", url="${book.service.url}")
public interface BooksClient {
@RequestMapping("/books")
List<Book> getBooks();
}
Теперь у нас есть клиент Feign, который получает список книг
из службы REST. Теперь давайте двигаться вперед и написать несколько интеграционных тестов.
3. WireMock
3.1. Настройка сервера WireMock
Если мы хотим протестировать наш BooksClient,
нам нужна фиктивная служба, предоставляющая конечную точку /books
. Наш клиент будет совершать вызовы против этой фиктивной службы. Для этой цели мы будем использовать WireMock.
Итак, давайте добавим зависимость WireMock Maven:
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
</dependency>
и настройте фиктивный сервер:
@TestConfiguration
public class WireMockConfig {
@Autowired
private WireMockServer wireMockServer;
@Bean(initMethod = "start", destroyMethod = "stop")
public WireMockServer mockBooksService() {
return new WireMockServer(9561);
}
}
Теперь у нас есть работающий фиктивный сервер, принимающий соединения через порт 9651.
3.2. Настройка макета
Давайте добавим свойство book.service.url
в наш application-test.yml,
указывающее на порт WireMockServer
:
book:
service:
url: http://localhost:9561
И давайте также подготовим фиктивный ответ get-books-response.json
для конечной точки /books
:
[
{
"title": "Dune",
"author": "Frank Herbert"
},
{
"title": "Foundation",
"author": "Isaac Asimov"
}
]
Давайте теперь настроим фиктивный ответ на запрос GET
в конечной точке /books
:
public class BookMocks {
public static void setupMockBooksResponse(WireMockServer mockService) throws IOException {
mockService.stubFor(WireMock.get(WireMock.urlEqualTo("/books"))
.willReturn(WireMock.aResponse()
.withStatus(HttpStatus.OK.value())
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(
copyToString(
BookMocks.class.getClassLoader().getResourceAsStream("payload/get-books-response.json"),
defaultCharset()))));
}
}
На данный момент все необходимые настройки готовы. Давайте продолжим и напишем наш первый тест.
4. Наш первый интеграционный тест
Создадим интеграционный тест BooksClientIntegrationTest
:
@SpringBootTest
@ActiveProfiles("test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { WireMockConfig.class })
class BooksClientIntegrationTest {
@Autowired
private WireMockServer mockBooksService;
@Autowired
private BooksClient booksClient;
@BeforeEach
void setUp() throws IOException {
BookMocks.setupMockBooksResponse(mockBooksService);
}
// ...
}
На данный момент у нас есть SpringBootTest
, настроенный с помощью WireMockServer
, готовый возвращать предопределенный список книг
, когда конечная точка /books
вызывается BooksClient
.
И, наконец, давайте добавим наши методы тестирования:
@Test
public void whenGetBooks_thenBooksShouldBeReturned() {
assertFalse(booksClient.getBooks().isEmpty());
}
@Test
public void whenGetBooks_thenTheCorrectBooksShouldBeReturned() {
assertTrue(booksClient.getBooks()
.containsAll(asList(
new Book("Dune", "Frank Herbert"),
new Book("Foundation", "Isaac Asimov"))));
}
5. Интеграция с лентой
Теперь давайте улучшим наш клиент, добавив возможности балансировки нагрузки, предоставляемые Ribbon.
Все, что нам нужно сделать в клиентском интерфейсе, это удалить жестко закодированный URL-адрес службы и вместо этого ссылаться на службу по имени службы book-service
:
@FeignClient("books-service")
public interface BooksClient {
...
Затем добавьте зависимость Netflix Ribbon Maven:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
И, наконец, в файле application-test.yml
теперь мы должны удалить book.service.url
и вместо этого определить ленту listOfServers
:
books-service:
ribbon:
listOfServers: http://localhost:9561
Давайте теперь снова запустим BooksClientIntegrationTest
. Это должно пройти, подтверждая, что новая настройка работает должным образом.
5.1. Динамическая конфигурация порта
Если мы не хотим жестко кодировать порт сервера, мы можем настроить WireMock на использование динамического порта при запуске.
Для этого создадим еще одну тестовую конфигурацию, RibbonTestConfig:
@TestConfiguration
@ActiveProfiles("ribbon-test")
public class RibbonTestConfig {
@Autowired
private WireMockServer mockBooksService;
@Autowired
private WireMockServer secondMockBooksService;
@Bean(initMethod = "start", destroyMethod = "stop")
public WireMockServer mockBooksService() {
return new WireMockServer(options().dynamicPort());
}
@Bean(name="secondMockBooksService", initMethod = "start", destroyMethod = "stop")
public WireMockServer secondBooksMockService() {
return new WireMockServer(options().dynamicPort());
}
@Bean
public ServerList ribbonServerList() {
return new StaticServerList<>(
new Server("localhost", mockBooksService.port()),
new Server("localhost", secondMockBooksService.port()));
}
}
Эта конфигурация устанавливает два сервера WireMock, каждый из которых работает на другом порту, динамически назначаемом во время выполнения. Кроме того, он также настраивает список серверов ленты с двумя фиктивными серверами.
5.2. Тестирование балансировки нагрузки
Теперь, когда у нас настроен балансировщик нагрузки ленты, давайте удостоверимся, что наш BooksClient
правильно переключается между двумя фиктивными серверами:
@SpringBootTest
@ActiveProfiles("ribbon-test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { RibbonTestConfig.class })
class LoadBalancerBooksClientIntegrationTest {
@Autowired
private WireMockServer mockBooksService;
@Autowired
private WireMockServer secondMockBooksService;
@Autowired
private BooksClient booksClient;
@BeforeEach
void setUp() throws IOException {
setupMockBooksResponse(mockBooksService);
setupMockBooksResponse(secondMockBooksService);
}
@Test
void whenGetBooks_thenRequestsAreLoadBalanced() {
for (int k = 0; k < 10; k++) {
booksClient.getBooks();
}
mockBooksService.verify(
moreThan(0), getRequestedFor(WireMock.urlEqualTo("/books")));
secondMockBooksService.verify(
moreThan(0), getRequestedFor(WireMock.urlEqualTo("/books")));
}
@Test
public void whenGetBooks_thenTheCorrectBooksShouldBeReturned() {
assertTrue(booksClient.getBooks()
.containsAll(asList(
new Book("Dune", "Frank Herbert"),
new Book("Foundation", "Isaac Asimov"))));
}
}
6. Эврика-интеграция
До сих пор мы видели, как тестировать клиент, использующий ленту для балансировки нагрузки. Но что , если в нашей установке используется система обнаружения служб, такая как Eureka. Мы должны написать интеграционный тест, который удостоверится, что наш BooksClient
работает должным образом и в таком контексте.
Для этого мы запустим сервер Eureka в качестве тестового контейнера . Затем мы запускаем и регистрируем фиктивный книжный сервис
в нашем контейнере Eureka. И, наконец, как только эта установка будет запущена, мы можем запустить наш тест на ней.
Прежде чем двигаться дальше, давайте добавим зависимости Testcontainers и Netflix Eureka Client Maven:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
6.1. Настройка тестового контейнера
Давайте создадим конфигурацию TestContainer, которая будет запускать наш сервер Eureka:
public class EurekaContainerConfig {
public static class Initializer implements ApplicationContextInitializer {
public static GenericContainer eurekaServer =
new GenericContainer("springcloud/eureka").withExposedPorts(8761);
@Override
public void initialize(@NotNull ConfigurableApplicationContext configurableApplicationContext) {
Startables.deepStart(Stream.of(eurekaServer)).join();
TestPropertyValues
.of("eureka.client.serviceUrl.defaultZone=http://localhost:"
+ eurekaServer.getFirstMappedPort().toString()
+ "/eureka")
.applyTo(configurableApplicationContext);
}
}
}
Как мы видим, приведенный выше инициализатор запускает контейнер. Затем он открывает порт 8761, который прослушивает сервер Eureka.
И, наконец, после запуска службы Eureka нам нужно обновить свойство eureka.client.serviceUrl.defaultZone
. Это определяет адрес сервера Eureka, используемого для обнаружения службы.
6.2. Зарегистрировать мок-сервер
Теперь, когда наш сервер Eureka запущен и работает, нам нужно зарегистрировать mock books-service
. Мы делаем это, просто создавая RestController:
@Configuration
@RestController
@ActiveProfiles("eureka-test")
public class MockBookServiceConfig {
@RequestMapping("/books")
public List getBooks() {
return Collections.singletonList(new Book("Hitchhiker's Guide to the Galaxy", "Douglas Adams"));
}
}
Все, что нам нужно сделать сейчас, чтобы зарегистрировать этот контроллер, — это убедиться, что свойство spring.application.name
в нашем application-eureka-test.yml
имеет значение books-service,
такое же, как имя службы, используемое в интерфейсе BooksClient.
.
Примечание. Теперь, когда библиотека netflix-eureka-client
находится в нашем списке зависимостей, Eureka будет использоваться по умолчанию для обнаружения служб. Итак, если мы хотим, чтобы наши предыдущие тесты, которые не используют Eureka, продолжали проходить, нам нужно вручную установить для eureka.client.enabled значение false
. Таким образом, даже если библиотека находится на пути, BooksClient
не будет пытаться использовать Eureka для поиска службы, а вместо этого будет использовать конфигурацию ленты.
6.3. Интеграционный тест
Еще раз, у нас есть все необходимые части конфигурации, поэтому давайте соберем их все вместе в тесте:
@ActiveProfiles("eureka-test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = { MockBookServiceConfig.class },
initializers = { EurekaContainerConfig.Initializer.class })
class ServiceDiscoveryBooksClientIntegrationTest {
@Autowired
private BooksClient booksClient;
@Lazy
@Autowired
private EurekaClient eurekaClient;
@BeforeEach
void setUp() {
await().atMost(60, SECONDS).until(() -> eurekaClient.getApplications().size() > 0);
}
@Test
public void whenGetBooks_thenTheCorrectBooksAreReturned() {
List books = booksClient.getBooks();
assertEquals(1, books.size());
assertEquals(
new Book("Hitchhiker's guide to the galaxy", "Douglas Adams"),
books.stream().findFirst().get());
}
}
В этом тесте происходит несколько вещей. Давайте посмотрим на них один за другим.
Во-первых, инициализатор контекста внутри EurekaContainerConfig
запускает службу Eureka.
Затем SpringBootTest
запускает приложение службы книг
, которое предоставляет контроллер, определенный в MockBookServiceConfig
.
Поскольку запуск контейнера Eureka и веб-приложения может занять несколько секунд , нам нужно дождаться регистрации службы книг
. Это происходит в настройках
теста.
И, наконец, метод тестов проверяет правильность работы BooksClient в сочетании с конфигурацией Eureka.
7. Заключение
В этой статье мы рассмотрели различные способы написания интеграционных тестов для клиента Spring Cloud Feign . Мы начали с базового клиента, который протестировали с помощью WireMock. После этого мы перешли к добавлению балансировки нагрузки с помощью ленты. Мы написали интеграционный тест и убедились, что наш Feign Client правильно работает с балансировкой нагрузки на стороне клиента, предоставляемой Ribbon. И, наконец, мы добавили в этот набор сервис обнаружения Eureka. И снова мы убедились, что наш клиент по-прежнему работает так, как ожидалось.
Как всегда, полный код доступен на GitHub .