Master Functional Testing in Spring Boot with Docker, WireMock, and Testcontainers
This comprehensive guide explains how to design and implement functional tests for a Spring Boot microservice using Docker containers, REST‑Assured, WireMock, and Testcontainers, covering architecture, code examples, test configuration, and best practices for maintainable black‑box testing.
Overview
This article demonstrates best practices for functional testing a Spring Boot microservice, focusing on black‑box testing without mocking the Spring context.
Theory
Functional testing verifies that software meets expected business requirements and produces the correct output for given inputs, ensuring usability and correctness of each feature.
Purpose
The tests should cover context startup, business requirements, and user stories, with each story having dedicated functional tests.
Practice
We build a sample service that stores and retrieves user details via a REST API and fetches contact information from an external service.
Architecture Design
The service uses Spring Boot, MariaDB, and a REST controller. The component diagram is illustrated below.
Implementation
Model classes
@Value(staticConstructor = "of")
public class UserDetails {
String firstName;
String lastName;
public static UserDetails fromEntity(UserDetailsEntity entity) {
return UserDetails.of(entity.getFirstName(), entity.getLastName());
}
public UserDetailsEntity toEntity(long userId) {
return new UserDetailsEntity(userId, firstName, lastName);
}
}REST API
@RestController
@RequestMapping("user")
@AllArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{userId}")
public User getUser(@PathVariable("userId") long userId) {
return userService.getUser(userId);
}
@PostMapping("/{userId}/details")
public void saveUserDetails(@PathVariable("userId") long userId,
@RequestBody UserDetails userDetails) {
userService.saveDetails(userId, userDetails);
}
@GetMapping("/{userId}/details")
public UserDetails getUserDetails(@PathVariable("userId") long userId) {
return userService.getDetails(userId);
}
}Database
The service persists data in MariaDB, but functional tests use a real MariaDB container via Testcontainers to avoid schema differences between H2 and production databases.
Functional Testing Setup
Two communication channels are tested:
Input channel – the service’s REST API, exercised with REST‑Assured .
Output channel – the external contacts service, stubbed with WireMock .
Testcontainers provides a real MariaDB instance for the tests.
Test Steps
Step objects encapsulate API calls:
@Component
public class UserDetailsServiceSteps implements ApplicationListener<WebServerInitializedEvent> {
private int servicePort;
public String getUser(long userId) {
return given().port(servicePort)
.when().get("user/" + userId)
.then().statusCode(200).contentType(ContentType.JSON)
.extract().asString();
}
public void saveUserDetails(long userId, String body) {
given().port(servicePort).body(body).contentType(ContentType.JSON)
.when().post("user/" + userId + "/details")
.then().statusCode(200);
}
@Override
public void onApplicationEvent(@NotNull WebServerInitializedEvent event) {
this.servicePort = event.getWebServer().getPort();
}
} @Component
public class ContactsServiceSteps {
public void expectGetUserContacts(long userId, String body) {
stubFor(get(urlPathMatching("/contacts"))
.withQueryParam("userId", equalTo(String.valueOf(userId)))
.willReturn(okJson(body)));
}
}Base Test Class
@RunWith(SpringRunner.class)
@SpringBootTest(classes = UserDetailsServiceApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public abstract class BaseFunctionalTest {
@Rule
public WireMockRule contactsServiceMock = new WireMockRule(options().port(8777));
@Autowired
protected UserDetailsServiceSteps userDetailsServiceSteps;
@Autowired
protected ContactsServiceSteps contactsServiceSteps;
@TestConfiguration
@ComponentScan("com.tdanylchuk.user.details.steps")
static class StepsConfiguration {}
}Example Test
public class RestUserDetailsTest extends BaseFunctionalTest {
private static final long USER_ID = 32343L;
private final String userContactsResponse = readFile("json/user-contacts.json");
private final String userDetails = readFile("json/user-details.json");
private final String expectedUserResponse = readFile("json/user.json");
@Test
public void shouldSaveUserDetailsAndRetrieveUser() throws Exception {
// when
userDetailsServiceSteps.saveUserDetails(USER_ID, userDetails);
// and
contactsServiceSteps.expectGetUserContacts(USER_ID, userContactsResponse);
// then
String actualUserResponse = userDetailsServiceSteps.getUser(USER_ID);
JSONAssert.assertEquals(expectedUserResponse, actualUserResponse, false);
}
}Conclusion
The guide shows how to build a Spring Boot microservice and verify its behavior with black‑box functional tests that use real Dockerized resources, ensuring tests remain reliable across environment changes and are easy to maintain.
Original source: https://dzone.com/articles/advanced-functional-testing-in-spring-boot-by-usin Author: Taras Danylchuk Translator: liumapp
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
