Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
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.
