Как писать интеграционные тесты с помощью Spring-Cloud-Netflix и симулировать

Я использую Spring-Cloud-Netflix для связи между микросервисами. Допустим, у меня есть две службы, Foo и Bar, а Foo использует одну из конечных точек REST Bar. Я использую интерфейс, помеченный @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 в eureka, чтобы fign смог найти его и связаться с ним? Какая магия аннотаций мне нужна?


person Bastian Voigt    schedule 13.09.2016    source источник
comment
Я пытался предложить вам ответ, но понимаю, что, скорее всего, ваша цель не совсем удачная. Если вы говорите об интеграционных тестах, то вам не нужно высмеивать BarClient логику. если вы это сделаете, то ваш тест будет модульным, а не интеграционным. А если это модульный тест, то вы можете BarClient просто имитировать с Mokito, вообще без HTTP-запросов. Я не понимаю, зачем вам http-запрос?   -  person Sergey Bespalov    schedule 22.09.2016
comment
Я не хочу создавать интеграционные тесты, которые объединяют несколько микросервисов. Когда я говорю «интеграционный тест», я имею в виду тестирование интеграции всех технических уровней в FooService, в отличие от модульных тестов, которые тестируют только один класс и заменяют остальные на макеты или заглушки.   -  person Bastian Voigt    schedule 22.09.2016
comment
Вы смотрели RestClientTest и MockRestServiceServer в Spring Boot 1.4?   -  person Tim    schedule 27.09.2016
comment
Вы нашли способ сделать это? Я пытаюсь добиться того же. Запуск микросервиса со всеми внешними зависимостями (например, сервер Eureka) имитируется вне процесса.   -  person Raipe    schedule 12.06.2017
comment
Как вы можете видеть в моем ответе ниже, я переключился на RestTemplate.   -  person Bastian Voigt    schedule 12.06.2017


Ответы (6)


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

Если вы используете 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();
    }
}

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

Чтобы тесты работали, нам нужно настроить балансировщик нагрузки Ribbon (будет использоваться внутренне клиентом 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.

person mladzo    schedule 30.11.2017
comment
Обратите внимание, что этот подход основан на отключении Hystrix по умолчанию, в противном случае резервный вариант может использоваться в реальном времени. Spring Cloud Dalston представил обязательный подход к подписке, но до этого Hystrix был включен по умолчанию. - person haggisandchips; 25.04.2018
comment
Это должен быть принятый ответ с условием, что он не проверяет десериализацию. - person haggisandchips; 25.04.2018
comment
У меня отлично сработало. Несколько замечаний: \ nЕсли вы используете JUnit 4.11 или выше, добавьте это: @Rule public WireMockClassRule instanceRule = wiremock - person Piyush; 14.02.2019
comment
Кроме того, если вы тестируете приложение Spring Boot 2, вы хотите зависеть от com.github.tomakehurst:wiremock-jre8:2.21.0 - person Piyush; 14.02.2019

Вот пример подключения Feign и 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 методе вашего теста.

person Alexander    schedule 11.08.2017
comment
Привет, спасибо за ответ! Это связано с Эврикой? Что такое google.url? - person Bastian Voigt; 13.08.2017
comment
Привет! google.url - это произвольное имя свойства для хранения URL-адреса службы зависимого объекта (в данном примере - домашняя страница Google). Эврика не принимается во внимание. Пришел к этому решению, пытаясь переопределить значение application.properties. - person Alexander; 14.08.2017

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

  1. Развертывание сервисов в тестовой среде и выполнение сквозных тестов
  2. Мокинг над другими микросервисами

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

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

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

person humbaba    schedule 01.12.2017

Я лично предпочитаю mockServer заглушить любой успокаивающий API, он прост в использовании и похож на wiremock, но он очень мощный по сравнению с последним.

Я приложил пример кода, написанного с помощью groovy / spock, для заглушки успокаивающего вызова GET с помощью mockServer.

Сначала автоматически подключите экземпляр mockServer в тестовом классе

@Autowired
private static ClientAndServer mockServer

запустите экземпляр mockServer из метода setupSpec (), этот метод аналогичен методу junit, аннотированному @BeforeClass.

def setupSpec() {
     mockServer = ClientAndServer.startClientAndServer(8080)
   }

определите необходимую заглушку в соответствующем модульном тесте

def "test case"() {
 given:
       new MockServerClient("localhost",8080).when(HttpRequest.request().withMethod("GET").withPath("/test/api").withQueryStringParameters(Parameter.param("param1", "param1_value"), Parameter.param("param2", "param2_value"))).respond(HttpResponse.response().withStatusCode(HttpStatus.OK.value()).withBody("{ message: 'sample response' }"))

 when:
 //your code
 then:
 //your code
}

после выполнения тестовых случаев остановите фиктивный сервер

def cleanupSpec() {
     mockServer.stop()
} 
person Siva Tharun    schedule 04.02.2020

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

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

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

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

person Sergey Bespalov    schedule 21.09.2016
comment
Привет, Сергей, спасибо за ответ! Добавление реестра eureka в мою службу звучит неплохо, но как я могу добавить к нему свой поддельный BarService? Учитывая ваше второе предложение, заменив BarClient на Mockito, да, я тоже хочу это сделать, но это для модульного теста. Я также хочу пройти интеграционный тест, включающий настоящую симуляцию магии. - person Bastian Voigt; 22.09.2016

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

@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, чем симулировать.

person Bastian Voigt    schedule 03.02.2017
comment
Но это не использует Feign, конечно, это действительно, если вы используете RestTemplate, но вопрос спрашивает о Feign with SpringBoot. Люди используют Feign, потому что это лучше, чем RestTemplate ... - person JeeBee; 10.01.2018
comment
Это не отвечает на исходный вопрос. Вы полностью изменили свою стратегию, что означает, что исходный вопрос к вам неприменим, но это все еще актуальный вопрос (и это именно то, что я пытаюсь сделать), и это НЕ ответ на него. - person haggisandchips; 25.04.2018
comment
@haggisandchips Как угодно. Для меня это решает проблему :) - person Bastian Voigt; 25.04.2018
comment
На самом деле не понимаю, почему все так ненавидят мой ответ. В конце концов, это был мой вопрос. И это мое решение, которое в то время отлично работало для меня. Во всяком случае, я больше не использую spring-cloud, потому что все это дерьмо и нестабильно. - person Bastian Voigt; 14.05.2018
comment
Возможно, если вы просто ответите на вопрос, люди проголосуют за. Кстати, если вы используете службы обнаружения, RestTemplate - действительно плохое решение, потому что они не знают об обнаружении из коробки. - person Martijn Hiemstra; 02.02.2019