Test Everything: Advanced Unit and Integration Testing with JUnit 5
Software testing has evolved far beyond the simple “write a test for each function” mindset. With modern architectures—microservices, cloud deployments, and distributed systems—applications require robust testing strategies that go deeper than unit tests. This is where JUnit 5 shines.
JUnit 5 is not just an update of JUnit 4; it’s a complete redesign that embraces modularity, extensibility, and advanced features for both unit and integration testing. Let’s explore how to master it.
Unit vs. Integration Testing: The Foundation
Before diving into code, let’s clarify the difference.
| Aspect | Unit Testing | Integration Testing |
|---|---|---|
| Scope | Individual methods or classes | Multiple components/modules working together |
| Dependencies | Mocked/stubbed | Real or semi-real (e.g., database, API) |
| Speed | Very fast | Slower due to environment setup |
| Tools in JUnit 5 | Assertions, parameterized tests, mocks (Mockito) | Testcontainers, Spring Test, real services |
In practice, a balanced strategy includes both—unit tests for correctness and integration tests for system reliability.
Advanced Unit Testing with JUnit 5
Nested Tests for Structure
JUnit 5 introduces @Nested tests, allowing you to organize related test cases more clearly.
import org.junit.jupiter.api.*;
class CalculatorTest {
@Nested
@DisplayName("Addition Tests")
class Addition {
@Test
void shouldAddTwoNumbers() {
Assertions.assertEquals(5, 2 + 3);
}
}
@Nested
@DisplayName("Subtraction Tests")
class Subtraction {
@Test
void shouldSubtractTwoNumbers() {
Assertions.assertEquals(1, 3 - 2);
}
}
}
This improves readability, especially in larger projects.
Parameterized Tests
Avoid repetitive test code by using @ParameterizedTest.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class PalindromeTest {
@ParameterizedTest
@ValueSource(strings = {"radar", "level", "madam"})
void shouldIdentifyPalindromes(String word) {
Assertions.assertTrue(new StringBuilder(word).reverse().toString().equals(word));
}
}
Using Mocks with Mockito
Mockito integrates seamlessly with JUnit 5 for isolating units under test
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;
class UserServiceTest {
@Test
void shouldFetchUserById() {
UserRepository repo = mock(UserRepository.class);
when(repo.findById(1)).thenReturn(new User(1, "Alice"));
UserService service = new UserService(repo);
User user = service.getUser(1);
Assertions.assertEquals("Alice", user.getName());
}
}
Integration Testing with JUnit 5
Testing with Spring Boot
JUnit 5 works perfectly with Spring Boot via @SpringBootTest.
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@SpringBootTest
class UserIntegrationTest {
@Autowired
private UserService userService;
@Test
void shouldCreateUser() {
User user = userService.createUser("Alice");
Assertions.assertNotNull(user.getId());
}
}
This spins up the Spring context, enabling full-stack integration testing.
Testcontainers for Real Environments
A common pain point in integration testing is database or message queue setup. With Testcontainers, you can spin up lightweight Docker containers inside tests.
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
class DatabaseIntegrationTest {
@Test
void testDatabaseConnection() {
try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")) {
postgres.start();
String jdbcUrl = postgres.getJdbcUrl();
// connect to db and run assertions
Assertions.assertTrue(jdbcUrl.contains("postgresql"));
}
}
}
This ensures integration tests run consistently across environments.
Best Practices
- Fast feedback: Keep unit tests lightweight and run them on every build.
- Layered strategy: Write more unit tests, fewer integration tests—but ensure integration covers critical flows.
- Use tags and filters:
@Tag("integration")
@Test
void slowTest() { ... }
Run them selectively with Maven/Gradle when needed.
4. Leverage extensions: JUnit 5’s Extension API allows custom lifecycle management, mocking, or logging.
Final Thoughts
JUnit 5 empowers developers to move beyond “just unit testing” into a world where integration testing is equally first-class. With nested tests, parameterization, dependency injection, and Testcontainers, you can build a comprehensive testing strategy that ensures both correctness and resilience.
Modern development demands: test everything. And with JUnit 5, that’s not just a mantra—it’s practical.

