This episode teaches quality assurance: unit testing with JUnit 5 and Mockito, integration testing with @SpringBootTest and @WebMvcTest, contract testing with Spring Cloud Contract, and Testcontainers for databases and service dependencies.

Code without tests is unfinished code. Episode 17 covers testing and quality assurance in Spring Boot — from fast unit tests to realistic integration tests with real containers.
You'll learn to write unit tests with JUnit 5 and Mockito, use slice tests like @WebMvcTest, test interactions between services with Testcontainers, and understand contract testing for microservices.
Spring Boot provides a test starter that bundles JUnit 5, Mockito, AssertJ, and more:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>A unit test focuses on a single class; dependencies are replaced with mocks. Here's an example testing a service that depends on a repository:
@ExtendWith(MockitoExtension.class)
class ItemServiceTest {
@Mock
private ItemRepository repository;
@InjectMocks
private ItemService service;
@Test
void cariItemBerdasarkanId() {
Item item = new Item(1L, "Laptop");
when(repository.findById(1L)).thenReturn(Optional.of(item));
Item hasil = service.findById(1L);
assertThat(hasil.getName()).isEqualTo("Laptop");
verify(repository).findById(1L);
}
}when(...).thenReturn(...) sets the mock behavior, and verify(...) ensures the method is actually called. This unit test runs in milliseconds without a database.
To make sure all beans are wired correctly, use @SpringBootTest — the test loads the entire application context:
@SpringBootTest
@AutoConfigureMockMvc
class ItemControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void listItemsMengembalikanStatusOk() throws Exception {
mockMvc.perform(get("/api/items"))
.andExpect(status().isOk());
}
}MockMvc sends fake HTTP requests without starting a real server — fast and deterministic. This test verifies that the controller, service, and other beans are wired together correctly.
To test a controller in isolation without loading the whole context, use @WebMvcTest. Only the web layer is loaded; other dependencies are mocked:
@WebMvcTest(ItemController.class)
class ItemControllerSliceTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ItemService service;
@Test
void getItemMengembalikan404() throws Exception {
when(service.findById(99L))
.thenThrow(new ItemNotFoundException(99L));
mockMvc.perform(get("/api/items/99"))
.andExpect(status().isNotFound());
}
}@WebMvcTest is much faster than @SpringBootTest because it only prepares the web layer. This test proves that the HTTP behavior — including the error handling from episode 7 — works correctly.
In-memory databases like H2 often behave differently from PostgreSQL. Testcontainers runs real Docker containers during tests, so tests run in the same environment as production:
@Testcontainers
@SpringBootTest
class ItemRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("belajar_test");
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private ItemRepository repository;
// test query sungguhan di PostgreSQL
}The PostgreSQL container starts before the test and stops after it. Datasource properties are pointed at the container via @DynamicPropertySource. Make sure Docker is running on your development machine.
Testcontainers also provides modules for Redis, Kafka, Elasticsearch, and more. The same pattern — declare a @Container, point properties at it, run the tests — applies to all service dependencies.
In microservices, an unexpected change in one service can break another. Contract testing ensures both sides honor an agreed contract. With Spring Cloud Contract, contracts are written on the producer side in a Groovy DSL — for example, a GET /api/items/1 request is expected to reply with status 200 and a body containing id and name.
This contract generates tests on the producer side and stubs on the consumer side. Any contract change is detected immediately at build time — before it breaks integrations at runtime.
A good strategy combines all levels:
./mvnw testThe command ./mvnw test runs the whole suite. In CI/CD (episode 21), this command becomes the gate before the application is deployed.
Episode 17 equipped you with quality assurance: unit testing with JUnit 5 and Mockito, integration testing with @SpringBootTest and @WebMvcTest, contract testing with Spring Cloud Contract, and Testcontainers for testing real databases and dependencies.
Key takeaways:
spring-boot-starter-test provides JUnit 5, Mockito, and AssertJ.@SpringBootTest loads the whole context; @WebMvcTest only the web layer.In the next episode, episode 18, we'll discuss modular architecture and patterns — clean, hexagonal, and onion architectures; feature module structure and package-by-feature; popular design patterns; and event-driven architecture with Spring Events and Kafka.