Перейти к содержанию

Интеграционные тесты с помощью Spring Cloud Netflix и Feign

В этой статье изучим интеграционное тестирование Feign Client.

Создадим базовый Open Feign Client, для которого напишем простой интеграционный тест с помощью WireMock.

После этого добавим конфигурацию Ribbon в наш клиент, а также создадим для него интеграционный тест. И, наконец, настроим тестовый контейнер Eureka и протестируем эту настройку, чтобы убедиться, что вся конфигурация работает должным образом.

Feign Client

Чтобы настроить Feign Client, необходимо сначала добавить Maven-зависимость Spring Cloud OpenFeign:

<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-сервиса. Теперь напишем несколько интеграционных тестов.

WireMock

Настройка сервера WireMock

Если хотим протестировать BooksClient, нам понадобится mock service, предоставляющий конечную точку /books. Наш клиент будет совершать вызовы этого mock service. Для этой цели будем использовать WireMock.

Добавим Maven-зависимость WireMock:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
</dependency>

И настроим mock server:

@TestConfiguration
public class WireMockConfig {

    @Autowired
    private WireMockServer wireMockServer;

    @Bean(initMethod = "start", destroyMethod = "stop")
    public WireMockServer mockBooksService() {
        return new WireMockServer(9561);
    }

}

Теперь у нас есть работающий mock server, принимающий соединения через порт 9651.

Настройка Mock

Добавим свойство book.service.url в application-test.yml, указывающее на порт WireMockServer:

book:
  service:
    url: http://localhost:9561

Подготовим mock-ответ get-books-response.json для конечной точки /books:

[
  {
    "title": "Dune",
    "author": "Frank Herbert"
  },
  {
    "title": "Foundation",
    "author": "Isaac Asimov"
  }
]

Теперь настроим mock-ответ на запрос 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()))));
    }

}

На данный момент все необходимые настройки готовы. Продолжим и напишем первый тест.

Первый интеграционный тест

Создадим интеграционный тест 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"))));
}

Интеграция с Ribbon

Улучшим наш клиент, добавив возможности балансировки нагрузки, предоставляемые Ribbon.

Все, что нужно сделать в клиентском интерфейсе, это удалить жестко закодированный URL-адрес сервиса и вместо него ссылаться на сервис по его имени book-service.

@FeignClient("books-service")
public interface BooksClient {
...

Добавим Maven-зависимость Netflix Ribbon:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

И, наконец, в файле application-test.yml необходимо удалить book.service.url и вместо него установить для Ribbon listOfServers:

books-service:
  ribbon:
    listOfServers: http://localhost:9561

Теперь снова запустим BooksClientIntegrationTest. Он должен пройти, подтверждая, что новая настройка работает должным образом.

Динамическая настройка порта

Если не хотим жестко указывать порт сервера, то можно настроить 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, каждый из которых работает на отдельном порту, динамически назначаемом во время выполнения. Кроме того, она также настраивает список серверов Ribbon с двумя mock server.

@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"))));
    }
}

Интеграция с Eureka

До сих пор мы видели, как тестировать клиент, использующий Ribbon для балансировки нагрузки. Но что, если в нашей установке используется система обнаружения сервисов, такая как Eureka. Мы должны написать интеграционный тест, который удостоверится, что BooksClient работает должным образом и в таком контексте.

Для этого запустим сервер Eureka в качестве тестового контейнера. Затем запускаем и регистрируем mock book-service в контейнере Eureka. И, наконец, как только эта установка будет запущена, можно запустить тест на ней.

Прежде чем двигаться дальше, добавим Maven-зависимости Testcontainers и Netflix Eureka Client:

<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>

Настройка тестового контейнера

Создадим конфигурацию 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, используемого для обнаружения сервиса.

Регистрация mock server

Теперь, когда сервер 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. Таким образом, даже если библиотека находится в path, BooksClient не будет пытаться использовать Eureka для поиска сервиса, а вместо этого будет использовать конфигурацию Ribbon.

Интеграционный тест

Теперь у нас есть все необходимые части конфигурации, поэтому соберем их все вместе в тесте:

@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 запускает приложение books-service, которое предоставляет контроллер, определенный в MockBookServiceConfig.

Поскольку запуск контейнера Eureka и веб-приложения может занять несколько секунд, необходимо дождаться регистрации books-service. Это происходит в настройках теста.

И, наконец, метод в тесте проверяет правильность работы BooksClient в сочетании с конфигурацией Eureka.

Заключение

В данной статье мы рассмотрели различные способы написания интеграционных тестов для Spring Cloud Feign Client. Мы начали с базового клиента, который протестировали с помощью WireMock. После этого перешли к добавлению балансировки нагрузки с помощью Ribbon. Написали интеграционный тест и убедились, что Feign Client правильно работает с балансировкой нагрузки на стороне клиента, предоставляемой Ribbon. И, наконец, добавили в этот набор сервис обнаружения Eureka. И снова убедились, что клиент по-прежнему работает так, как ожидалось.

Полный код доступен на GitHub.

Оригинал