Как писать интеграционные тесты с 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 ответов:
Вот пример использования 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, но вы можете использовать другие варианты для настройки тестовой среды, которая вам нужна.
- в тестовой среде вы можете развернуть реестр службы Eureka в отдельном контейнере сервлета Jetty, и все аннотации будут работать так же, как и в реальной рабочей среде.
- Если вы не хотите использовать реальную логику конечных точек
BarClient
, а интеграционный тест касается только реального транспортного уровняhttp
, то вы можно использовать Mockito для заглушки конечной точкиBarClient
.Я полагаю,что для реализации 1 и 2 с помощью Spring-Boot вам потребуется создать два отдельных приложения для тестовой среды. Для Юрика реестра службы под пристанью, а другой-для
Другим решением является ручная настройка Jetty и Eureka в контексте тестового приложения. Я думаю, что это лучший способ, но в таком случае вы должны понять, что такоеBarClient
конечная точка тоже заглушки под причал.@EnableEurekaServer
и@EnableDiscoveryClient
аннотации сделайте с контекстом приложения Spring.
Раньше было в основном два варианта выполнения интеграционных тестов для приложений микросервисов:
- развертывание служб в тестовой среде и выполнение сквозных операций тесты
- издеваясь над другими микрослужб
Первый вариант имеет очевидный недостаток-хлопоты по развертыванию всех зависимостей (других служб, баз данных и т. д.). Кроме того, он медленный и трудно отлаживается.
Второй вариант быстрее и имеет меньше хлопот, но это легко сделать. в итоге получаются заглушки, которые ведут себя иначе, чем реальность во времени, из-за возможных изменений кода. Таким образом, можно иметь успешные тесты, но неудачное приложение при развертывании в 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, чем притворяться.