Рубрики
Без рубрики

Интеграционные тесты с Spring Cloud Netflix и Feign

Давайте рассмотрим, как мы можем провести интеграционное тестирование фальшивого клиента.

Автор оригинала: Adrian Maghear.

1. Обзор

В этой статье мы рассмотрим интеграционное тестирование притворного клиента .

Мы создадим базовый Открытый клиент симуляции для которого мы напишем простой интеграционный тест с помощью WireMock .

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

2. Притворный Клиент

Чтобы настроить наш FeignClient, мы должны сначала добавить зависимость Spring Cloud Open Feign Maven:


    org.springframework.cloud
    spring-cloud-starter-openfeign

После этого давайте создадим класс Book для нашей модели:

public class Book {
    private String title;
    private String author;
}

И, наконец, давайте создадим наш притворный клиентский интерфейс:

@FeignClient(value="simple-books-client", url="${book.service.url}")
public interface BooksClient {

    @RequestMapping("/books")
    List getBooks();

}

Теперь у нас есть Притворный клиент, который извлекает список Книг из службы REST. Теперь давайте двигаться вперед и напишем несколько интеграционных тестов.

3. WireMock

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

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

Итак, давайте добавим зависимость WireMock Maven:


    com.github.tomakehurst
    wiremock
    test

и настройте макет сервера:

@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. Наш Первый Интеграционный Тест

Давайте создадим интеграционный тест Books Client Integration Test :

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

    // ...
}

На данный момент у нас есть Spring Boot Test , настроенный с помощью WireMockServer готов вернуть заранее определенный список Книг , когда конечная точка /книг вызывается 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:


    org.springframework.cloud
    spring-cloud-starter-netflix-ribbon

И, наконец, в файле application-test.yml теперь мы должны удалить book.service.url и вместо этого определить ленту listOfServers :

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

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

5.1. Динамическая конфигурация Порта

Если мы не хотим жестко кодировать порт сервера, мы можем настроить WireMock на использование динамического порта при запуске.

Для этого давайте создадим еще одну тестовую конфигурацию, Ribbon Test Config:

@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. Тестирование балансировки Нагрузки

Теперь, когда у нас настроен балансировщик нагрузки на ленту, давайте убедимся, что наш клиент Books правильно чередуется между двумя фиктивными серверами:

@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. Мы должны написать интеграционный тест, который гарантирует, что наш клиент Books работает так, как ожидалось в таком контексте.

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

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


    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client


    org.testcontainers
    testcontainers
    test

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 запущен и работает, нам нужно зарегистрировать макет 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.имя свойства в нашем приложении-eureka-test.yml является books-service, таким же, как имя службы, используемое в интерфейсе BooksClient .

Примечание: Теперь, когда библиотека netflix-eureka-client находится в нашем списке зависимостей, Eureka будет использоваться по умолчанию для обнаружения служб. Итак, если мы хотим, чтобы наши предыдущие тесты, которые не используют Eureka, продолжали проходить, нам нужно будет вручную установить eureka.client.enabled в false . Таким образом, даже если библиотека находится на пути, клиент Books не будет пытаться использовать 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());
    }

}

В этом тесте происходит несколько вещей. Давайте рассмотрим их по очереди.

Во-первых, инициализатор контекста внутри Eureka ContainerConfig запускает службу Eureka.

Затем тест загрузки Spring запускает приложение books-service , которое предоставляет контроллер, определенный в MockBookServiceConfig .

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

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

7. Заключение

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

Как всегда, полный код доступен на GitHub .