1. Overview

In this article, we’re going to explore the integration testing of a Feign Client.

We’ll create a basic Open Feign Client for which we’ll write a simple integration test with the help of WireMock.

After that, we’ll add a Ribbon configuration to our client and also build an integration test for it. And finally, we’ll configure a Eureka test container and test this setup to make sure our entire configuration works as expected.

2. The Feign Client

To set up our Feign Client, we should first add the Spring Cloud OpenFeign Maven dependency:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

After that, let’s create a Book class for our model:

public class Book {
    private String title;
    private String author;
}

And finally, let’s create our Feign Client interface:

@FeignClient(name = "books-service")
public interface BooksClient {

    @RequestMapping("/books")
    List<Book> getBooks();
}

Now, we have a Feign Client that retrieves a list of Books from a REST service. Now, let’s move forward and write some integration tests.

3. WireMock

3.1. Setting up the WireMock Server

If we want to test our BooksClient, we need a mock service that provides the /books endpoint. Our client will make calls against this mock service. For this purpose, we’ll use WireMock.

So, let’s add the WireMock Maven dependency:

<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock-standalone</artifactId>
    <scope>test</scope>
</dependency>

and configure the mock server:

@TestConfiguration
@ActiveProfiles("test")
public class WireMockConfig {

    @Bean(initMethod = "start", destroyMethod = "stop")
    public WireMockServer mockBooksService() {
        return new WireMockServer(80);
    }

    @Bean(initMethod = "start", destroyMethod = "stop")
    public WireMockServer mockBooksService2() {
        return new WireMockServer(81);
    }
}

We now have two running mock servers accepting connections on port 80 and 81.

3.2. Setting up the Mock

Let’s add the properties book-service url to our application-test.yml pointing to the WireMockServer port:

spring:
  application:
    name: books-service
  cloud:
    loadbalancer:
      ribbon:
        enabled: false
    discovery:
      client:
        simple:
          instances:
            books-service[0]:
              uri: http://localhost:80
            books-service[1]:
              uri: http://localhost:81

And let’s also prepare a mock response get-books-response.json for the /books endpoint:

[
  {
    "title": "Dune",
    "author": "Frank Herbert"
  },
  {
    "title": "Foundation",
    "author": "Isaac Asimov"
  }
]

Let’s now configure the mock response for a GET request on the /books endpoint:

public class BookMocks {

    public static void setupMockBooksResponse(WireMockServer mockService) throws IOException {
        mockService.stubFor(WireMock.get(WireMock.urlEqualTo("/books"))
          .willReturn(WireMock.aResponse()
            .withStatus(HttpStatus.OK.value())
            .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
            .withBody(
              copyToString(
                BookMocks.class.getClassLoader().getResourceAsStream("payload/get-books-response.json"),
                defaultCharset()))));
    }

}

At this point, all the required configuration is in place. Let’s go ahead and write our first test.

4. Our First Integration Test

Let’s create an integration test BooksClientIntegrationTest:

@SpringBootTest
@ActiveProfiles("test")
@EnableFeignClients
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { WireMockConfig.class })
class BooksClientIntegrationTest {

    @Autowired
    private WireMockServer mockBooksService;

    @Autowired
    private WireMockServer mockBooksService2;

    @Autowired
    private BooksClient booksClient;

    @BeforeEach
    void setUp() throws IOException {
        setupMockBooksResponse(mockBooksService);
        setupMockBooksResponse(mockBooksService2);
    }
    //...
}

At this point, we have a SpringBootTest configured with a WireMockServer ready to return a predefined list of Books when the /books endpoint is invoked by the BooksClient.

And finally, let’s add our test methods:

@Test
public void whenGetBooks_thenBooksShouldBeReturned() {
    assertFalse(booksClient.getBooks().isEmpty());
}

@Test
public void whenGetBooks_thenTheCorrectBooksShouldBeReturned() {
    assertTrue(booksClient.getBooks()
      .containsAll(asList(
        new Book("Dune", "Frank Herbert"),
        new Book("Foundation", "Isaac Asimov"))));
}

5. Integrating with Spring Cloud LoadBalancer

Now let’s improve our client by adding the load-balancing capabilities provided by Spring Cloud LoadBalancer.

All we need to do in the client interface is to remove the hard-coded service URL and instead refer to the service by the service name book-service:

@FeignClient(name= "books-service")
public interface BooksClient {
...

Next, add the maven dependency:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

And finally, in the application-test.yml file:

spring:
  application:
    name: books-service
  cloud:
    loadbalancer:
      ribbon:
        enabled: false
    discovery:
      client:
        simple:
          instances:
            books-service[0]:
              uri: http://localhost:80
            books-service[1]:
              uri: http://localhost:81

Let’s now run the BooksClientIntegrationTest again. It should pass, confirming the new setup works as expected.

5.1. Dynamic Port Configuration

If we don’t want to hard-code the server’s port, we can configure WireMock to use a dynamic port at startup.

For this, let’s create another test configuration, TestConfig:

@TestConfiguration
@ActiveProfiles("test")
public class TestConfig {

    @Bean(initMethod = "start", destroyMethod = "stop")
    public WireMockServer mockBooksService() {
        return new WireMockServer(options().port(80));
    }

    @Bean(name="secondMockBooksService", initMethod = "start", destroyMethod = "stop")
    public WireMockServer secondBooksMockService() {
        return new WireMockServer(options().port(81));
    }
}

This configuration sets up two WireMock servers, each running on a different port dynamically assigned at runtime. Moreover, it also configures the Ribbon server list with the two mock servers.

5.2. Load Balancing Testing

Now that we have our Ribbon load balancer configured, let’s make sure our BooksClient correctly alternates between the two mock servers:

@SpringBootTest
@ActiveProfiles("test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { TestConfig.class })
class LoadBalancerBooksClientIntegrationTest {

    @Autowired
    private WireMockServer mockBooksService;

    @Autowired
    private WireMockServer secondMockBooksService;

    @Autowired
    private BooksClient booksClient;

    @Autowired
    private LoadBalancerClientFactory clientFactory;

    @BeforeEach
    void setUp() throws IOException {
        setupMockBooksResponse(mockBooksService);
        setupMockBooksResponse(secondMockBooksService);

        String serviceId = "books-service";
        RoundRobinLoadBalancer loadBalancer = new RoundRobinLoadBalancer(ServiceInstanceListSuppliers
          .toProvider(serviceId, instance(serviceId, "localhost", false), instance(serviceId, "localhost", true)),
          serviceId, -1);
    }
  
    private static DefaultServiceInstance instance(String serviceId, String host, boolean secure) {
        return new DefaultServiceInstance(serviceId, serviceId, host, 80, secure);
    }

    @Test
    void whenGetBooks_thenRequestsAreLoadBalanced() {
        for (int k = 0; k < 10; k++) {
            booksClient.getBooks();
        }

        mockBooksService.verify(
          moreThan(0), getRequestedFor(WireMock.urlEqualTo("/books")));
        secondMockBooksService.verify(
          moreThan(0), getRequestedFor(WireMock.urlEqualTo("/books")));
    }

    @Test
    public void whenGetBooks_thenTheCorrectBooksShouldBeReturned() {
        assertTrue(booksClient.getBooks()
          .containsAll(asList(
            new Book("Dune", "Frank Herbert"),
            new Book("Foundation", "Isaac Asimov"))));
    }
}

6. Eureka Integration

We have seen, so far, how to test a client that uses Spring Cloud LoadBalancer for load balancing. But what if our setup uses a service discovery system like Eureka. We should write an integration test that makes sure that our BooksClient works as expected in such a context also.

For this purpose, we’ll run a Eureka server as a test container. Then we startup and register a mock book-service with our Eureka container. And finally, once this installation is up, we can run our test against it.

Before moving further, let’s add the Testcontainers and Netflix Eureka Client Maven dependencies:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <scope>test</scope>
</dependency>

6.1. TestContainer Setup

Let’s create a TestContainer configuration that will spin up our Eureka server:

public class EurekaContainerConfig {

    public static class Initializer implements ApplicationContextInitializer {

        public static GenericContainer eurekaServer = 
          new GenericContainer("springcloud/eureka").withExposedPorts(8761);

        @Override
        public void initialize(@NotNull ConfigurableApplicationContext configurableApplicationContext) {

            Startables.deepStart(Stream.of(eurekaServer)).join();

            TestPropertyValues
              .of("eureka.client.serviceUrl.defaultZone=http://localhost:" 
                + eurekaServer.getFirstMappedPort().toString() 
                + "/eureka")
              .applyTo(configurableApplicationContext);
        }
    }
}

As we can see, the initializer above starts the container. Then it exposes port 8761, on which the Eureka server is listening.

And finally, after the Eureka service has started, we need to update the eureka.client.serviceUrl.defaultZone property. This defines the address of the Eureka server used for service discovery.

6.2. Register Mock Server

Now that our Eureka server is up and running we need to register a mock books-service. We do this by simply creating a RestController:

@Configuration
@RestController
@ActiveProfiles("eureka-test")
public class MockBookServiceConfig {

    @RequestMapping("/books")
    public List getBooks() {
        return Collections.singletonList(new Book("Hitchhiker's Guide to the Galaxy", "Douglas Adams"));
    }
}

All we have to do now, in order to register this controller, is to make sure the spring.application.name property in our application-eureka-test.yml is books-service, the same as the service name used in the BooksClient interface.

Note: Now that the netflix-eureka-client library is in our list of dependencies, Eureka will be used by default for service discovery. So, if we want our previous tests, that don’t use Eureka, to keep passing, we’ll need to manually set eureka.client.enabled to false. In that way, even if the library is on the path, the BooksClient will not try to use Eureka for locating the service, but instead, use the Ribbon configuration.

6.3. Integration Test

Once again, we have all the needed configuration pieces, so let’s put them all together in a test:

@ActiveProfiles("eureka-test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class, webEnvironment =  SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = { MockBookServiceConfig.class }, 
  initializers = { EurekaContainerConfig.Initializer.class })
class ServiceDiscoveryBooksClientIntegrationTest {

    @Autowired
    private BooksClient booksClient;

    @Lazy
    @Autowired
    private EurekaClient eurekaClient;

    @BeforeEach
    void setUp() {
        await().atMost(60, SECONDS).until(() -> eurekaClient.getApplications().size() > 0);
    }

    @Test
    public void whenGetBooks_thenTheCorrectBooksAreReturned() {
        List books = booksClient.getBooks();

        assertEquals(1, books.size());
        assertEquals(
          new Book("Hitchhiker's guide to the galaxy", "Douglas Adams"), 
          books.stream().findFirst().get());
    }

}

There are a few things happening in this test. Let’s look at them one by one.

Firstly, the context initializer inside EurekaContainerConfig starts the Eureka service.

Then, the SpringBootTest starts the books-service application that exposes the controller defined in MockBookServiceConfig.

Because the startup of the Eureka container and the web application can take a few seconds, we need to wait until the books-service gets registered. This happens in the setUp of the test.

And finally, the tests method verifies that the BooksClient indeed works correctly in combination with the Eureka configuration.

7. Conclusion

In this article, we’ve explored the different ways we can write integration tests for a Spring Cloud Feign Client. We started with a basic client which we tested with the help of WireMock. After that, we moved on to adding load balancing with Ribbon. We wrote an integration test and made sure our Feign Client works correctly with the client-side load balancing provided by Ribbon. And finally, we added Eureka service discovery to the mix. And again, we made sure our client still works as expected.

As always, the complete code is available over on GitHub.