Testing Spring Cloud Feign clients with Eureka

Lately I feel there’s so much technology coming out every single day while abandoning, or at least not talking enough about one critical software development activity, testing.

If some tech pops up, we shall discuss the related testing practices and while Feign is not a new technology in the Java world, I still wanted to write a couple articles because the community definitely doesn’t have enough content on its testing.

This time, I’ll talk about testing Spring Cloud OpenFeign clients integrated with Spring Cloud Netflix Eureka for service resolution.

A simple Feign client

Let’s check out a simple client with Spring Cloud OpenFeign. Say we have an inventory-service component responsible for handling products, their stock values and so on. We have an online-store-service component as well which is exposing APIs to the public to be able to create products, buy products, delete products and amongst those, it does other things too.

An example architecture

When the online-store-service wants to communicate with the inventory-service, we could write a Feign client like this:

@FeignClient(name = "inventory-service", url = "http://localhost:8081")
public interface InventoryServiceClient {
    @PostMapping(value = "/products", produces = MediaType.APPLICATION_JSON_VALUE)
    @HandleFeignException(ProductCreationFailedExceptionHandler.class)
    CreateProductResponse createProduct(CreateProductRequest request);
}

Note: the @HandleFeignException annotation is used from another article: Maintainable error handling with Feign.

Specifying the url attribute in the @FeignClient (http://localhost:8081) annotation means we’re saying that the service the Feign client represents is located on that particular address.

Not very robust.

Eureka? Why?

What can we do? Thankfully the Spring Cloud Netflix Eureka service-registry can seamlessly act as a logical service name resolver.

What if we could dynamically resolve the physical address of a service instance instead of hardcoding it? Spring Cloud Netflix Eureka with Spring Cloud OpenFeign is doing just that.

The previous example with a tiny change:

@FeignClient(name = "inventory-service")
public interface InventoryServiceClient {
    @PostMapping(value = "/products", produces = MediaType.APPLICATION_JSON_VALUE)
    @HandleFeignException(ProductCreationFailedExceptionHandler.class)
    CreateProductResponse createProduct(CreateProductRequest request);
}

That’s it, I just removed the url attribute from the @FeignClient annotation.

Now, if the inventory-service has started up and registered itself with the name inventory-service into Eureka, when starting up the online-store-service with this particular Feign client implementation, what will happen during invocation is, the logical inventory-service name in the @FeignClient annotation will resolve to a physical service address, like http://localhost:8081 or http://192.168.1.56:8080 or wherever the inventory-service is located.

This way we can decouple the microservices from the concrete infrastructure deployment and focus on what’s important, creating value.

Okay okay, we all know that Eureka works really well with Feign, how about testing?

A simple Feign client test

First let’s try to test a normal Feign client with a fixed URL, without Eureka.

The idea is to test whether the request/response mapping is set up properly, error handling of the client and so on.

To simulate a service instance, I’ll use WireMock and start a mock server on port 8081.

@SpringBootTest({"server.port:0"})
public class InventoryServiceClientTest {
    @RegisterExtension
    static WireMockExtension INVENTORY_SERVICE = WireMockExtension.newInstance()
            .options(WireMockConfiguration.wireMockConfig().port(8081))
            .build();
}

Then the test case will be simple, calling the Feign client with the parameters and from the mock server, we’re returning back some response body, then the test will verify if the response is properly mapped.

@SpringBootTest({"server.port:0"})
public class InventoryServiceClientTest {
    @RegisterExtension
    static WireMockExtension INVENTORY_SERVICE = WireMockExtension.newInstance()
            .options(WireMockConfiguration.wireMockConfig().port(8081))
            .build();

    @Autowired
    private InventoryServiceClient inventoryServiceClient;

    @Test
    public void testInventoryServiceClientCreateProductWorksProperly() {
        String responseBody = "{ \"productId\": \"828bc3cb-52f0-482b-8247-d3db5c87c941\", \"name\": \"Phone\", \"stock\": 3}";
        INVENTORY_SERVICE.stubFor(WireMock.post("/products").withRequestBody(WireMock.equalToJson("{ \"name\": \"Phone\", \"initialStock\": 3}"))
                .willReturn(WireMock.okJson(responseBody)));

        CreateProductResponse response = inventoryServiceClient.createProduct(new CreateProductRequest("Phone", 3));

        assertThat(response.getProductId()).isNotNull();
        assertThat(response.getName()).isEqualTo("Phone");
        assertThat(response.getStock()).isEqualTo(3);
    }
}

Note that the stubbing happens for a specific request body JSON meaning that I’m implicitly validating wherher the Feign invocation will transform the parameters properly into JSON format.

This is rather easy and I’d encourage everybody to test your Feign clients to make sure you’re handling positive scenarios properly as well as error cases. Microservices will fail, you gotta be prepared for failures, otherwise errors will spread across your system.

Creative testing with Eureka

What about a Feign client test with Eureka? Well, you could try to provide a URL value using SpEL and set it only from the tests but in my opinion that’s not a very good approach.

You could also think about, okay, let’s just start up a Eureka server from the test too and register an arbitrary inventory-service instance that will point to the WireMock server. You’re partially right but still not good enough. Eureka and its clients have internal caches which will expire from time-to-time and there are a number of factors coming into play to decide when a service instance shows up in Eureka or stays in the client cache.

Instead of those approaches, what if we could simulate a very simplistic fixed logical service name resolution? That’s what we’re going to do.

The idea is to set up a list of logical names which resolve to a fixed physical address list, like with a Map. We’re going to implement a translation table and set it for Spring Cloud for use.

The service name resolution is using the ServiceInstanceListSupplier interface for Spring Cloud OpenFeign. We’ll create a test implementation from this which will work easily with the WireMock server.

Since in the above case we’re testing a single Feign client, we’ll be able to set up only a single service type (inventory-service) but not multiple ones although it’s fairly easy to adjust the implementation to support multiple service names.

public class TestServiceInstanceListSupplier implements ServiceInstanceListSupplier {
    private String serviceId;
    private int[] ports;

    public TestServiceInstanceListSupplier(String serviceId, int... ports) {
        this.serviceId = serviceId;
        this.ports = ports;
    }

    @Override
    public String getServiceId() {
        return serviceId;
    }

    @Override
    public Flux<List<ServiceInstance>> get() {
        List<ServiceInstance> result = new ArrayList<>();
        for (int i = 0; i < ports.length; i++) {
            result.add(new DefaultServiceInstance(serviceId + i, getServiceId(), "localhost", ports[i], false));
        }
        return Flux.just(result);
    }
}

This implementation however supports multiple ports on the same host for the same service, meaning that this version can be used for testing load-balancing with WireMock.

Last piece is to register this as a Spring bean within the test and make sure to disable the Eureka client:

@SpringBootTest({"server.port:0", "eureka.client.enabled:false"})
public class InventoryServiceClientTest {

    @TestConfiguration
    public static class TestConfig {
        @Bean
        public ServiceInstanceListSupplier serviceInstanceListSupplier() {
            return new TestServiceInstanceListSupplier("inventory-service", 8081);
        }
    }

    @RegisterExtension
    static WireMockExtension INVENTORY_SERVICE = WireMockExtension.newInstance()
            .options(WireMockConfiguration.wireMockConfig().port(8081))
            .build();

    @Autowired
    private InventoryServiceClient inventoryServiceClient;

    @Test
    public void testInventoryServiceClientCreateProductWorksProperly() {
        String responseBody = "{ \"productId\": \"828bc3cb-52f0-482b-8247-d3db5c87c941\", \"name\": \"Phone\", \"stock\": 3}";
        INVENTORY_SERVICE.stubFor(WireMock.post("/products").withRequestBody(WireMock.equalToJson("{ \"name\": \"Phone\", \"initialStock\": 3}"))
                .willReturn(WireMock.okJson(responseBody)));

        CreateProductResponse response = inventoryServiceClient.createProduct(new CreateProductRequest("Phone", 3));

        assertThat(response.getProductId()).isNotNull();
        assertThat(response.getName()).isEqualTo("Phone");
        assertThat(response.getStock()).isEqualTo(3);
    }
}

And that’s it. Now the Feign client doesn’t have the url attribute in the annotation but everything is set like it’s in production when it comes to the Feign client at least.

You can find the full test-case right here on GitHub. It was created for my new course: Mastering microservice communication with Spring Cloud Feign, check it out if you’re interesting in more things on Feign.

Don’t forget to follow me on Facebook, Twitter and subscribe to the newsletter for more exciting stuff.

Leave a Reply

Your email address will not be published.