How to write integration tests with spring-cloud-netflix and feign
There used to be basically two options for doing integration tests for microservices applications:
- Deployment of services to a test environment and make end-to-end tests
- Mocking other microservices
First option has the obvious disadvantage of the hassle of deploying all the dependencies (other services, databases, etc) as well. In addition, it is slow and hard to debug.
Second option is faster and has less hassle but it is easy to end up with stubs that behave differently than the reality in time, due to possible code changes. So it is possible to have successful tests but failing app when deployed to prod.
A better solution would be using consumer driven contract verification, so that you will make sure that provider service's API is compliant with the consumer calls. For this purpose, Spring developers can use Spring Cloud Contract. For other environments, there is a framework called PACT. Both can be used with Feign clients as well. Here is an example with PACT.
Here is an example how to do the wiring of Feign and WireMock with random port (based on Spring-Boot github answer).
@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()
);
}
}
}
Alternatively you can try playing with System.setProperty()
in @BeforeClass
method of your test.
Here is an example of using WireMock to test SpringBoot configuration with Feign client and Hystrix fallback.
If you are using Eureka as a server discovery, you need to disable it by setting a property "eureka.client.enabled=false"
.
First, we need to enable the Feign/Hystrix configuration for our application:
@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();
}
}
Please note that we are specifying a fallback class for the Feign client. Fallback class will be called every time Feign client call fails (e.g. connection timeout).
In order for tests to work, we need to configure the Ribbon loadbalancer (will be used internally by Feign client when sending http request):
@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()));
}
}
}
Ribbon server list need to match the url (host and port) of our WireMock configuration.