Как писать интеграционные тесты с spring-cloud-netflix и feign


Я использую Spring-Cloud-Netflix для связи между микрослужбами. Допустим, у меня есть две службы, Foo и Bar,и Foo потребляет одну из конечных точек отдыха бара. Я использую интерфейс с аннотацией @FeignClient:

@FeignClient
public interface BarClient {
  @RequestMapping(value = "/some/url", method = "POST")
  void bazzle(@RequestBody BazzleRequest);
}

Тогда у меня есть класс обслуживания SomeService в Foo, который вызывает BarClient.

@Component
public class SomeService {
    @Autowired
    BarClient barClient;

    public String doSomething() {
      try {
        barClient.bazzle(new BazzleRequest(...));
        return "so bazzle my eyes dazzle";
      } catch(FeignException e) {
        return "Not bazzle today!";
      }

    }
}
Теперь, чтобы убедиться, что связь между службами работает, я хочу построить тест, который запускает настоящий HTTP-запрос против поддельного сервера Bar, используя что-то вроде WireMock. Тест должен убедитесь, что приложение feign правильно декодирует ответ службы и передает его в SomeService.
public class SomeServiceIntegrationTest {

    @Autowired SomeService someService;

    @Test
    public void shouldSucceed() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(204);

      String result = someService.doSomething();

      assertThat(result, is("so bazzle my eyes dazzle"));
    }

    @Test
    public void shouldFail() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(404);

      String result = someService.doSomething();

      assertThat(result, is("Not bazzle today!"));
    }
}

Как я могу внедрить такой сервер WireMock в Эврику, чтобы симулировать его и общаться с ним? Какая магия аннотаций мне нужна?

5 14

5 ответов:

Вот пример использования WireMock для тестирования конфигурации SpringBoot с помощью Feign client и Hystrix fallback.

Если вы используете Eureka в качестве обнаружения сервера, вам нужно отключить его, установив свойство "eureka.client.enabled=false".

Во-первых, нам нужно включить конфигурацию Feign/Hystrix для нашего приложения:

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@FeignClient(
        name = "bookstore-server",
        fallback = BookClientFallback.class,
        qualifier = "bookClient"
)
public interface BookClient {

    @RequestMapping(method = RequestMethod.GET, path = "/book/{id}")
    Book findById(@PathVariable("id") String id);
}

@Component
public class BookClientFallback implements BookClient {

    @Override
    public Book findById(String id) {
        return Book.builder().id("fallback-id").title("default").isbn("default").build();
    }
}

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

Для того, чтобы тесты работали, нам нужно настроить ленточный loadbalancer (будет использоваться внутренне клиентом Feign при отправке http-запроса):

@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
        "feign.hystrix.enabled=true"
})
@ContextConfiguration(classes = {BookClientTest.LocalRibbonClientConfiguration.class})
public class BookClientTest {

    @Autowired
    public BookClient bookClient;

    @ClassRule
    public static WireMockClassRule wiremock = new WireMockClassRule(
            wireMockConfig().dynamicPort()));

    @Before
    public void setup() throws IOException {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withHeader("Content-Type", MediaType.APPLICATION_JSON)
                        .withBody(StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("fixtures/book.json"), Charset.defaultCharset()))));
    }

    @Test
    public void testFindById() {
        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("12345"));
    }

    @Test
    public void testFindByIdFallback() {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse().withFixedDelay(60000)));

        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("fallback-id"));
    }

    @TestConfiguration
    public static class LocalRibbonClientConfiguration {
        @Bean
        public ServerList<Server> ribbonServerList() {
            return new StaticServerList<>(new Server("localhost", wiremock.port()));
        }
    }
}

Список ленточных серверов должен соответствовать url-адресу (хосту и Порту) нашей конфигурации WireMock.

Вот пример, как сделать проводку симуляции и WireMock со случайным портом (на основе Spring-Boot GitHub ответа).

@RunWith(SpringRunner.class)
@SpringBootTest(properties = "google.url=http://google.com") // emulate application.properties
@ContextConfiguration(initializers = PortTest.RandomPortInitializer.class)
@EnableFeignClients(clients = PortTest.Google.class)
public class PortTest {

    @ClassRule
    public static WireMockClassRule wireMockRule = new WireMockClassRule(
        wireMockConfig().dynamicPort()
    );

    @FeignClient(name = "google", url = "${google.url}")
    public interface Google {    
        @RequestMapping(method = RequestMethod.GET, value = "/")
        String request();
    }

    @Autowired
    public Google google;

    @Test
    public void testName() throws Exception {
        stubFor(get(urlEqualTo("/"))
                .willReturn(aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withBody("Hello")));

        assertEquals("Hello", google.request());
    }


    public static class RandomPortInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {

            // If the next statement is commented out, 
            // Feign will go to google.com instead of localhost
            TestPropertySourceUtils
                .addInlinedPropertiesToEnvironment(applicationContext,
                    "google.url=" + "http://localhost:" + wireMockRule.port()
            );
        }
    }
}

В качестве альтернативы Вы можете попробовать поиграть с System.setProperty() в @BeforeClass методе вашего теста.

Вероятно, нет способа сделать WireMock comunicate непосредственно с сервером Eureka, но вы можете использовать другие варианты для настройки тестовой среды, которая вам нужна.

  1. в тестовой среде вы можете развернуть реестр службы Eureka в отдельном контейнере сервлета Jetty, и все аннотации будут работать так же, как и в реальной рабочей среде.
  2. Если вы не хотите использовать реальную логику конечных точек BarClient, а интеграционный тест касается только реального транспортного уровня http, то вы можно использовать Mockito для заглушки конечной точки BarClient.

Я полагаю,что для реализации 1 и 2 с помощью Spring-Boot вам потребуется создать два отдельных приложения для тестовой среды. Для Юрика реестра службы под пристанью, а другой-для BarClient конечная точка тоже заглушки под причал.

Другим решением является ручная настройка Jetty и Eureka в контексте тестового приложения. Я думаю, что это лучший способ, но в таком случае вы должны понять, что такое @EnableEurekaServer и @EnableDiscoveryClient аннотации сделайте с контекстом приложения Spring.

Раньше было в основном два варианта выполнения интеграционных тестов для приложений микросервисов:

  1. развертывание служб в тестовой среде и выполнение сквозных операций тесты
  2. издеваясь над другими микрослужб

Первый вариант имеет очевидный недостаток-хлопоты по развертыванию всех зависимостей (других служб, баз данных и т. д.). Кроме того, он медленный и трудно отлаживается.

Второй вариант быстрее и имеет меньше хлопот, но это легко сделать. в итоге получаются заглушки, которые ведут себя иначе, чем реальность во времени, из-за возможных изменений кода. Таким образом, можно иметь успешные тесты, но неудачное приложение при развертывании в prod.

Лучшим решением было бы использование проверки контракта, управляемой потребителем, чтобы убедиться, что API службы поставщика соответствует вызовам потребителя. Для этой цели разработчики Spring могут использоватьSpring Cloud Contract . Для других сред существует фреймворк под названием пакт . И то и другое можно использовать и с притворными клиентами. Вот пример с пактом.

RestTemplate

Использовать весной вместо "сымитировать". RestTemplate также может разрешать имена служб через eureka, поэтому вы можете сделать что-то вроде этого:

@Component
public class SomeService {
   @Autowired
   RestTemplate restTemplate;

   public String doSomething() {
     try {
       restTemplate.postForEntity("http://my-service/some/url", 
                                  new BazzleRequest(...), 
                                  Void.class);
       return "so bazzle my eyes dazzle";
     } catch(HttpStatusCodeException e) {
       return "Not bazzle today!";
     }
   }
}

Это гораздо легче проверить с помощью Wiremock, чем притворяться.