JUnit 5 Test Structure in Java

Table of Contents

  1. Introduction to JUnit 5
  2. Project Setup and Dependencies
  3. Basic Test Structure
  4. Test Lifecycle Annotations
  5. Assertions and Assumptions
  6. Parameterized Tests
  7. Test Interfaces and Default Methods
  8. Dynamic Tests
  9. Testing Best Practices
  10. Integration with Spring Boot

Introduction to JUnit 5

JUnit 5 is the next generation of the popular JUnit testing framework for Java. It consists of several different modules from three different sub-projects:

  • JUnit Platform: Test execution foundation
  • JUnit Jupiter: New programming model and extension model
  • JUnit Vintage: Support for running JUnit 3 and 4 tests

Key Benefits:

  • Rich set of annotations for flexible test configuration
  • Powerful assertions with better error messages
  • Parameterized tests for data-driven testing
  • Extension model for custom functionality
  • Improved test lifecycle control

Project Setup and Dependencies

1. Maven Dependencies

<!-- pom.xml -->
<properties>
<junit.version>5.10.0</junit.version>
<mockito.version>5.5.0</mockito.version>
<assertj.version>3.24.2</assertj.version>
</properties>
<dependencies>
<!-- JUnit 5 Jupiter API -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 Jupiter Engine -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 Params for Parameterized Tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Mockito for Mocking -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<!-- AssertJ for Fluent Assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>

2. Gradle Configuration

// build.gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0'
testImplementation 'org.mockito:mockito-core:5.5.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.5.0'
testImplementation 'org.assertj:assertj-core:3.24.2'
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
showStandardStreams = true
}
}

Basic Test Structure

1. Simple Test Class

// CalculatorTest.java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Calculator Tests")
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
System.out.println("Setting up before each test");
}
@AfterEach
void tearDown() {
calculator = null;
System.out.println("Cleaning up after each test");
}
@Test
@DisplayName("Addition of two positive numbers")
void testAddition_PositiveNumbers() {
// Arrange
int a = 5;
int b = 3;
// Act
int result = calculator.add(a, b);
// Assert
assertEquals(8, result, "5 + 3 should equal 8");
}
@Test
@DisplayName("Addition with zero")
void testAddition_WithZero() {
assertEquals(5, calculator.add(5, 0), "5 + 0 should equal 5");
assertEquals(3, calculator.add(0, 3), "0 + 3 should equal 3");
assertEquals(0, calculator.add(0, 0), "0 + 0 should equal 0");
}
@Test
@DisplayName("Subtraction of numbers")
void testSubtraction() {
assertEquals(2, calculator.subtract(5, 3), "5 - 3 should equal 2");
assertEquals(-2, calculator.subtract(3, 5), "3 - 5 should equal -2");
assertEquals(0, calculator.subtract(5, 5), "5 - 5 should equal 0");
}
@Test
@DisplayName("Multiplication of numbers")
void testMultiplication() {
assertAll("multiplication",
() -> assertEquals(15, calculator.multiply(5, 3), "5 * 3 should equal 15"),
() -> assertEquals(0, calculator.multiply(5, 0), "5 * 0 should equal 0"),
() -> assertEquals(-15, calculator.multiply(5, -3), "5 * -3 should equal -15")
);
}
@Test
@DisplayName("Division by zero should throw exception")
void testDivision_ByZero() {
Exception exception = assertThrows(ArithmeticException.class, 
() -> calculator.divide(10, 0),
"Division by zero should throw ArithmeticException"
);
assertEquals("/ by zero", exception.getMessage());
}
@Test
@Disabled("Not implemented yet")
@DisplayName("Square root calculation - TODO")
void testSquareRoot() {
// TODO: Implement this test
fail("Not implemented yet");
}
}
// Calculator.java (Class under test)
class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("/ by zero");
}
return a / b;
}
}

2. Nested Test Classes

// UserServiceTest.java
import org.junit.jupiter.api.*;
import java.time.LocalDateTime;
import java.util.List;
@DisplayName("User Service Tests")
class UserServiceTest {
private UserService userService;
private User testUser;
@BeforeEach
void setUp() {
userService = new UserService();
testUser = new User(1L, "[email protected]", "John", "Doe", true);
}
@Nested
@DisplayName("User Creation Tests")
class UserCreationTests {
@Test
@DisplayName("Create valid user")
void createUser_WithValidData_ShouldSucceed() {
// Act
User createdUser = userService.createUser("[email protected]", "Jane", "Smith");
// Assert
assertNotNull(createdUser.getId());
assertEquals("[email protected]", createdUser.getEmail());
assertEquals("Jane", createdUser.getFirstName());
assertEquals("Smith", createdUser.getLastName());
assertTrue(createdUser.isActive());
}
@Test
@DisplayName("Create user with invalid email should fail")
void createUser_WithInvalidEmail_ShouldFail() {
// Act & Assert
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> userService.createUser("invalid-email", "John", "Doe")
);
assertEquals("Invalid email format", exception.getMessage());
}
}
@Nested
@DisplayName("User Retrieval Tests")
class UserRetrievalTests {
@BeforeEach
void setUpUsers() {
userService.createUser("[email protected]", "Alice", "Johnson");
userService.createUser("[email protected]", "Bob", "Williams");
}
@Test
@DisplayName("Find user by existing ID")
void findUserById_WithExistingId_ShouldReturnUser() {
// Act
User foundUser = userService.findUserById(1L);
// Assert
assertNotNull(foundUser);
assertEquals(1L, foundUser.getId());
}
@Test
@DisplayName("Find user by non-existing ID should return null")
void findUserById_WithNonExistingId_ShouldReturnNull() {
// Act
User foundUser = userService.findUserById(999L);
// Assert
assertNull(foundUser);
}
@Test
@DisplayName("Find all active users")
void findAllActiveUsers_ShouldReturnOnlyActiveUsers() {
// Arrange
userService.deactivateUser(1L);
// Act
List<User> activeUsers = userService.findAllActiveUsers();
// Assert
assertEquals(1, activeUsers.size());
assertTrue(activeUsers.stream().allMatch(User::isActive));
}
}
@Nested
@DisplayName("User Update Tests")
class UserUpdateTests {
@Test
@DisplayName("Update user email")
void updateUserEmail_WithValidEmail_ShouldSucceed() {
// Act
User updatedUser = userService.updateUserEmail(1L, "[email protected]");
// Assert
assertEquals("[email protected]", updatedUser.getEmail());
}
}
}
// User.java
class User {
private Long id;
private String email;
private String firstName;
private String lastName;
private boolean active;
private LocalDateTime createdAt;
public User(Long id, String email, String firstName, String lastName, boolean active) {
this.id = id;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
this.active = active;
this.createdAt = LocalDateTime.now();
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
public LocalDateTime getCreatedAt() { return createdAt; }
}
// UserService.java
class UserService {
public User createUser(String email, String firstName, String lastName) {
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
// In real implementation, this would save to database
return new User(1L, email, firstName, lastName, true);
}
public User findUserById(Long id) {
// Mock implementation
return id == 1L ? new User(1L, "[email protected]", "Test", "User", true) : null;
}
public List<User> findAllActiveUsers() {
// Mock implementation
return List.of(
new User(2L, "[email protected]", "Active", "User", true)
);
}
public void deactivateUser(Long id) {
// Mock implementation
}
public User updateUserEmail(Long id, String newEmail) {
// Mock implementation
return new User(id, newEmail, "Test", "User", true);
}
}

Test Lifecycle Annotations

1. Complete Test Lifecycle

// TestLifecycleExampleTest.java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Test Lifecycle Example")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestLifecycleExampleTest {
private static int staticCounter = 0;
private int instanceCounter = 0;
// This runs once before any test methods in the class
@BeforeAll
static void beforeAll() {
staticCounter++;
System.out.println("BeforeAll - Static counter: " + staticCounter);
System.out.println("This runs once before all tests");
}
// This runs once after all test methods in the class
@AfterAll
static void afterAll() {
System.out.println("AfterAll - All tests completed");
System.out.println("Static counter: " + staticCounter);
}
// This runs before each test method
@BeforeEach
void beforeEach() {
instanceCounter++;
System.out.println("BeforeEach - Instance counter: " + instanceCounter);
}
// This runs after each test method
@AfterEach
void afterEach() {
System.out.println("AfterEach - Test completed");
}
@Test
@DisplayName("First test method")
void firstTest() {
System.out.println("Executing first test");
assertEquals(1, instanceCounter);
assertTrue(true);
}
@Test
@DisplayName("Second test method")
void secondTest() {
System.out.println("Executing second test");
assertEquals(2, instanceCounter);
assertFalse(false);
}
@Test
@DisplayName("Third test method")
void thirdTest() {
System.out.println("Executing third test");
assertEquals(3, instanceCounter);
}
}

2. Test Instance Lifecycle Modes

// TestInstanceLifecycleTest.java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisplayName("PER_CLASS Lifecycle Example")
class TestInstanceLifecycleTest {
private int sharedCounter = 0;
private static int staticCounter = 0;
public TestInstanceLifecycleTest() {
System.out.println("Constructor called - Instance created");
staticCounter++;
}
@BeforeAll
void beforeAll() {
System.out.println("BeforeAll - Shared counter: " + sharedCounter);
// In PER_CLASS, @BeforeAll can be instance method
}
@BeforeEach
void beforeEach() {
sharedCounter++;
System.out.println("BeforeEach - Shared counter: " + sharedCounter);
}
@Test
@DisplayName("Test 1 - Should see incremented counter")
void test1() {
System.out.println("Test 1 - Shared counter: " + sharedCounter);
assertEquals(1, sharedCounter);
assertEquals(1, staticCounter);
}
@Test
@DisplayName("Test 2 - Should see incremented counter from previous test")
void test2() {
System.out.println("Test 2 - Shared counter: " + sharedCounter);
assertEquals(2, sharedCounter);
assertEquals(1, staticCounter); // Same instance
}
@AfterAll
void afterAll() {
System.out.println("AfterAll - Final shared counter: " + sharedCounter);
System.out.println("Static counter: " + staticCounter);
}
}
// DefaultLifecycleTest.java
@DisplayName("PER_METHOD Lifecycle Example (Default)")
class DefaultLifecycleTest {
private int instanceCounter = 0;
private static int staticCounter = 0;
public DefaultLifecycleTest() {
System.out.println("Constructor called - Instance created");
staticCounter++;
}
@BeforeAll
static void beforeAll() {
System.out.println("BeforeAll - Static counter: " + staticCounter);
}
@BeforeEach
void beforeEach() {
instanceCounter++;
System.out.println("BeforeEach - Instance counter: " + instanceCounter);
}
@Test
@DisplayName("Test 1 - Fresh instance")
void test1() {
System.out.println("Test 1 - Instance counter: " + instanceCounter);
assertEquals(1, instanceCounter);
}
@Test
@DisplayName("Test 2 - Fresh instance")
void test2() {
System.out.println("Test 2 - Instance counter: " + instanceCounter);
assertEquals(1, instanceCounter); // Fresh instance
}
@AfterAll
static void afterAll() {
System.out.println("AfterAll - Total instances created: " + staticCounter);
}
}

Assertions and Assumptions

1. JUnit 5 Assertions

// AssertionsTest.java
import org.junit.jupiter.api.*;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
class AssertionsTest {
private List<String> names;
private Map<String, Integer> scores;
@BeforeEach
void setUp() {
names = Arrays.asList("Alice", "Bob", "Charlie", "David");
scores = Map.of("Alice", 95, "Bob", 87, "Charlie", 92);
}
@Test
@DisplayName("Basic assertions")
void basicAssertions() {
// Equality assertions
assertEquals(4, 2 + 2);
assertEquals("Hello", "Hello", "Message on failure");
assertNotEquals(5, 2 + 2);
// Boolean assertions
assertTrue(5 > 3);
assertFalse(5 < 3);
// Null assertions
String nullString = null;
String notNullString = "Hello";
assertNull(nullString);
assertNotNull(notNullString);
// Same instance assertions
Object actual = new Object();
Object expected = actual;
Object different = new Object();
assertSame(expected, actual);
assertNotSame(actual, different);
}
@Test
@DisplayName("Collection assertions")
void collectionAssertions() {
// List assertions
assertAll("names",
() -> assertEquals(4, names.size()),
() -> assertTrue(names.contains("Alice")),
() -> assertFalse(names.contains("Eve")),
() -> assertLinesMatch(
Arrays.asList("Alice", "Bob", "Charlie", "David"),
names
)
);
// Map assertions
assertAll("scores",
() -> assertEquals(3, scores.size()),
() -> assertEquals(95, scores.get("Alice")),
() -> assertTrue(scores.containsKey("Bob")),
() -> assertFalse(scores.containsKey("Eve"))
);
}
@Test
@DisplayName("Grouped assertions")
void groupedAssertions() {
Person person = new Person("John", "Doe", 30);
// All assertions are executed, and failures are reported together
assertAll("person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName()),
() -> assertEquals(30, person.getAge()),
() -> assertTrue(person.getAge() > 18, "Should be adult")
);
}
@Test
@DisplayName("Exception assertions")
void exceptionAssertions() {
Calculator calculator = new Calculator();
// Test that exception is thrown
ArithmeticException exception = assertThrows(ArithmeticException.class, 
() -> calculator.divide(10, 0)
);
// Test exception message
assertEquals("/ by zero", exception.getMessage());
// Test no exception is thrown
assertDoesNotThrow(() -> calculator.add(5, 3));
}
@Test
@DisplayName("Timeout assertions")
void timeoutAssertions() {
// Test completes within timeout
assertTimeout(Duration.ofSeconds(1), () -> {
// Simulate some work
Thread.sleep(500);
});
// Test result within timeout
String result = assertTimeout(Duration.ofSeconds(1), () -> {
Thread.sleep(500);
return "Completed";
});
assertEquals("Completed", result);
// Preemptively fail if timeout exceeded
assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
// This would fail preemptively
Thread.sleep(50);
});
}
@Test
@DisplayName("Optional assertions")
void optionalAssertions() {
Optional<String> present = Optional.of("value");
Optional<String> empty = Optional.empty();
assertTrue(present.isPresent());
assertFalse(empty.isPresent());
assertEquals("value", present.get());
}
@Test
@DisplayName("Array assertions")
void arrayAssertions() {
int[] expected = {1, 2, 3, 4, 5};
int[] actual = {1, 2, 3, 4, 5};
int[] different = {1, 2, 3, 4, 6};
assertArrayEquals(expected, actual);
assertFalse(Arrays.equals(expected, different));
}
}
// Person.java
class Person {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
}

2. AssertJ Fluent Assertions

// AssertJTest.java
import org.junit.jupiter.api.*;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
class AssertJTest {
private List<String> names;
private Map<String, Integer> scores;
@BeforeEach
void setUp() {
names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Alice");
scores = Map.of("Alice", 95, "Bob", 87, "Charlie", 92);
}
@Test
@DisplayName("String assertions with AssertJ")
void stringAssertions() {
String text = "Hello World";
assertThat(text)
.isNotNull()
.isNotEmpty()
.hasSize(11)
.startsWith("Hello")
.endsWith("World")
.contains("llo Wor")
.doesNotContain("Goodbye")
.isEqualTo("Hello World")
.isNotEqualTo("Goodbye World");
}
@Test
@DisplayName("Collection assertions with AssertJ")
void collectionAssertions() {
assertThat(names)
.isNotNull()
.hasSize(5)
.contains("Alice", "Bob")
.containsExactly("Alice", "Bob", "Charlie", "David", "Alice")
.containsSequence("Bob", "Charlie")
.doesNotContain("Eve")
.hasOnlyElementsOfType(String.class)
.containsOnlyOnce("Bob", "Charlie", "David")
.contains("Alice") // Can appear multiple times
.filteredOn(name -> name.startsWith("A"))
.containsOnly("Alice");
assertThat(scores)
.hasSize(3)
.containsKeys("Alice", "Bob")
.containsValue(95)
.doesNotContainKey("Eve")
.containsEntry("Alice", 95);
}
@Test
@DisplayName("Object assertions with AssertJ")
void objectAssertions() {
Person person = new Person("John", "Doe", 30);
assertThat(person)
.isNotNull()
.hasFieldOrProperty("firstName")
.hasFieldOrPropertyWithValue("lastName", "Doe")
.extracting(Person::getFirstName, Person::getAge)
.containsExactly("John", 30);
assertThat(person.getFirstName())
.isEqualTo("John")
.isNotEqualTo("Jane");
assertThat(person.getAge())
.isGreaterThan(18)
.isLessThan(65)
.isBetween(25, 35);
}
@Test
@DisplayName("Exception assertions with AssertJ")
void exceptionAssertions() {
Calculator calculator = new Calculator();
// Check exception is thrown
assertThatThrownBy(() -> calculator.divide(10, 0))
.isInstanceOf(ArithmeticException.class)
.hasMessage("/ by zero");
// Alternative syntax
assertThatExceptionOfType(ArithmeticException.class)
.isThrownBy(() -> calculator.divide(10, 0))
.withMessage("/ by zero");
// Check no exception is thrown
assertThatCode(() -> calculator.add(5, 3))
.doesNotThrowAnyException();
}
@Test
@DisplayName("Optional assertions with AssertJ")
void optionalAssertions() {
Optional<String> present = Optional.of("value");
Optional<String> empty = Optional.empty();
assertThat(present)
.isPresent()
.contains("value")
.hasValue("value");
assertThat(empty).isEmpty();
}
@Test
@DisplayName("Date and time assertions with AssertJ")
void dateTimeAssertions() {
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
LocalDate yesterday = today.minusDays(1);
assertThat(today)
.isBefore(tomorrow)
.isAfter(yesterday)
.isEqualTo(LocalDate.now())
.isBetween(yesterday, tomorrow);
}
@Test
@DisplayName("Custom assertions with AssertJ")
void customAssertions() {
Person person = new Person("John", "Doe", 30);
// Using custom assertion
assertThat(person)
.hasFirstName("John")
.hasLastName("Doe")
.isAdult();
}
// Custom AssertJ assertions for Person class
private AbstractPersonAssert<?> assertThat(Person actual) {
return new AbstractPersonAssert<>(actual, AbstractPersonAssert.class);
}
static class AbstractPersonAssert<SELF extends AbstractPersonAssert<SELF>> 
extends AbstractObjectAssert<SELF, Person> {
public AbstractPersonAssert(Person actual, Class<?> selfType) {
super(actual, selfType);
}
public SELF hasFirstName(String firstName) {
isNotNull();
if (!actual.getFirstName().equals(firstName)) {
failWithMessage("Expected first name to be <%s> but was <%s>", 
firstName, actual.getFirstName());
}
return myself;
}
public SELF hasLastName(String lastName) {
isNotNull();
if (!actual.getLastName().equals(lastName)) {
failWithMessage("Expected last name to be <%s> but was <%s>", 
lastName, actual.getLastName());
}
return myself;
}
public SELF isAdult() {
isNotNull();
if (actual.getAge() < 18) {
failWithMessage("Expected person to be adult (age >= 18) but was <%s>", 
actual.getAge());
}
return myself;
}
}
}

3. Assumptions

// AssumptionsTest.java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assumptions.*;
import static org.junit.jupiter.api.Assertions.*;
class AssumptionsTest {
@Test
@DisplayName("Test only on development environment")
void testOnlyOnDevEnvironment() {
String env = System.getenv("APP_ENV");
assumeTrue("dev".equals(env), 
"Skipping test: Not in development environment");
// This part only runs in dev environment
assertEquals(2, 1 + 1);
}
@Test
@DisplayName("Test only on specific operating system")
void testOnlyOnWindows() {
String os = System.getProperty("os.name").toLowerCase();
assumingThat(os.contains("win"), () -> {
// This block only runs on Windows
System.out.println("Running Windows-specific test");
assertEquals(2, 1 + 1);
});
// This part always runs
assertTrue(true);
}
@Test
@DisplayName("Test with assumption that data is available")
void testWithDataAssumption() {
String databaseUrl = System.getenv("DATABASE_URL");
assumeFalse(databaseUrl == null || databaseUrl.isEmpty(), 
"Skipping test: Database URL not available");
// Test database operations
assertTrue(databaseUrl.startsWith("jdbc:"));
}
@Test
@DisplayName("Test with multiple assumptions")
void testWithMultipleAssumptions() {
String env = System.getenv("APP_ENV");
String databaseUrl = System.getenv("DATABASE_URL");
assumeTrue("ci".equals(env) || "dev".equals(env),
"Skipping test: Not in CI or development environment");
assumeTrue(databaseUrl != null && !databaseUrl.isEmpty(),
"Skipping test: Database not available");
// Run the actual test
assertTrue(true);
}
}

Parameterized Tests

1. Basic Parameterized Tests

// ParameterizedTestExamples.java
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class ParameterizedTestExamples {
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE})
@DisplayName("Test with odd numbers")
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
assertTrue(Math.abs(number % 2) == 1);
}
@ParameterizedTest
@ValueSource(strings = {"", "  "})
@DisplayName("Test with blank strings")
void isBlank_ShouldReturnTrueForBlankStrings(String input) {
assertTrue(input == null || input.trim().isEmpty());
}
@ParameterizedTest
@NullAndEmptySource
@DisplayName("Test with null and empty strings")
void isBlank_ShouldReturnTrueForNullAndEmptyStrings(String input) {
assertTrue(input == null || input.trim().isEmpty());
}
@ParameterizedTest
@NullSource
@DisplayName("Test with null value")
void isNull_ShouldReturnTrueForNull(Object input) {
assertNull(input);
}
@ParameterizedTest
@EmptySource
@DisplayName("Test with empty collections")
void isEmpty_ShouldReturnTrueForEmptyCollections(List<String> list) {
assertTrue(list.isEmpty());
}
@ParameterizedTest
@MethodSource("stringProvider")
@DisplayName("Test with method source")
void testWithMethodSource(String argument) {
assertNotNull(argument);
assertTrue(argument.length() > 0);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana", "cherry");
}
@ParameterizedTest
@MethodSource("rangeProvider")
@DisplayName("Test with range of numbers")
void testWithRange(int number) {
assertTrue(number >= 1 && number <= 5);
}
static IntStream rangeProvider() {
return IntStream.range(1, 6);
}
}

2. Advanced Parameterized Tests

// AdvancedParameterizedTests.java
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import java.time.LocalDate;
import java.util.EnumSet;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class AdvancedParameterizedTests {
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"5, 5, 10",
"10, -5, 5"
})
@DisplayName("Addition with CSV source")
void add_WithCsvSource(int a, int b, int expected) {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.add(a, b));
}
@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1)
@DisplayName("Addition with CSV file source")
void add_WithCsvFileSource(int a, int b, int expected) {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.add(a, b));
}
@ParameterizedTest
@EnumSource(TimeUnit.class)
@DisplayName("Test with enum source")
void testWithEnumSource(TimeUnit timeUnit) {
assertNotNull(timeUnit);
assertTrue(timeUnit.name().length() > 0);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = {"DAYS", "HOURS"})
@DisplayName("Test with specific enum values")
void testWithSpecificEnumValues(TimeUnit timeUnit) {
assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EnumSource.Mode.EXCLUDE, names = {"DAYS", "HOURS"})
@DisplayName("Test with excluded enum values")
void testWithExcludedEnumValues(TimeUnit timeUnit) {
assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}
@ParameterizedTest
@ArgumentsSource(CustomArgumentsProvider.class)
@DisplayName("Test with custom arguments provider")
void testWithCustomArgumentsProvider(String input, int length) {
assertEquals(length, input.length());
}
static class CustomArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(
Arguments.of("apple", 5),
Arguments.of("banana", 6),
Arguments.of("cherry", 6)
);
}
}
@ParameterizedTest
@MethodSource("complexArgumentsProvider")
@DisplayName("Test with complex arguments")
void testWithComplexArguments(LocalDate date, String description, boolean isFuture) {
if (isFuture) {
assertTrue(date.isAfter(LocalDate.now()));
} else {
assertTrue(date.isBefore(LocalDate.now()) || date.isEqual(LocalDate.now()));
}
assertNotNull(description);
}
static Stream<Arguments> complexArgumentsProvider() {
return Stream.of(
Arguments.of(LocalDate.now().plusDays(1), "Tomorrow", true),
Arguments.of(LocalDate.now().minusDays(1), "Yesterday", false),
Arguments.of(LocalDate.now(), "Today", false)
);
}
@ParameterizedTest(name = "{0} + {1} = {2}")
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"5, 5, 10"
})
@DisplayName("Addition with custom display name")
void add_WithCustomDisplayName(int a, int b, int expected) {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.add(a, b));
}
}

Test Interfaces and Default Methods

1. Test Interface with Default Methods

// TestLifecycleLogger.java
import org.junit.jupiter.api.*;
import java.util.logging.Logger;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public interface TestLifecycleLogger {
Logger LOG = Logger.getLogger(TestLifecycleLogger.class.getName());
@BeforeAll
default void beforeAllTests() {
LOG.info("Before all tests in " + getClass().getSimpleName());
}
@AfterAll
default void afterAllTests() {
LOG.info("After all tests in " + getClass().getSimpleName());
}
@BeforeEach
default void beforeEachTest(TestInfo testInfo) {
LOG.info(() -> String.format("About to execute [%s]",
testInfo.getDisplayName()));
}
@AfterEach
default void afterEachTest(TestInfo testInfo) {
LOG.info(() -> String.format("Finished executing [%s]",
testInfo.getDisplayName()));
}
}
// TimingExtension.java
import org.junit.jupiter.api.extension.*;
import java.lang.reflect.Method;
import java.util.logging.Logger;
public interface TimingExtension extends BeforeTestExecutionCallback, AfterTestExecutionCallback {
Logger LOG = Logger.getLogger(TimingExtension.class.getName());
String START_TIME = "start time";
@Override
default void beforeTestExecution(ExtensionContext context) {
getStore(context).put(START_TIME, System.currentTimeMillis());
}
@Override
default void afterTestExecution(ExtensionContext context) {
Method testMethod = context.getRequiredTestMethod();
long startTime = getStore(context).remove(START_TIME, long.class);
long duration = System.currentTimeMillis() - startTime;
LOG.info(() -> String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
}
private ExtensionContext.Store getStore(ExtensionContext context) {
return context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod()));
}
}
// DatabaseTest.java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
public interface DatabaseTest {
@BeforeEach
default void setUpDatabase() {
System.out.println("Setting up database connection for test");
// In real implementation, this would initialize database connection
}
@AfterEach
default void tearDownDatabase() {
System.out.println("Closing database connection after test");
// In real implementation, this would close database connection
}
default void cleanDatabase() {
System.out.println("Cleaning database tables");
// In real implementation, this would clean test data
}
}
// Implementing test classes with interfaces
@DisplayName("User Service Tests with Interfaces")
class UserServiceInterfaceTest implements TestLifecycleLogger, TimingExtension, DatabaseTest {
private UserService userService;
@BeforeEach
void setUp() {
userService = new UserService();
cleanDatabase(); // From DatabaseTest interface
}
@Test
@DisplayName("Create user with interface defaults")
void createUser_WithInterfaceDefaults() {
User user = userService.createUser("[email protected]", "John", "Doe");
assertNotNull(user);
assertEquals("[email protected]", user.getEmail());
}
@Test
@DisplayName("Find user by ID with interface defaults")
void findUserById_WithInterfaceDefaults() {
User user = userService.findUserById(1L);
// This might be null in mock implementation, but test should pass
assertTrue(user == null || user.getId() != null);
}
}

Dynamic Tests

1. Dynamic Test Generation

// DynamicTestsExample.java
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.function.Executable;
import java.util.*;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
class DynamicTestsExample {
@TestFactory
@DisplayName("Dynamic tests from collection")
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertTrue(true)),
dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
);
}
@TestFactory
@DisplayName("Dynamic tests from iterable")
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(
dynamicTest("3rd dynamic test", () -> assertTrue(true)),
dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))
);
}
@TestFactory
@DisplayName("Dynamic tests from stream")
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("apple", "banana", "cherry")
.map(text -> dynamicTest("Test length of: " + text, 
() -> assertTrue(text.length() > 0)));
}
@TestFactory
@DisplayName("Dynamic tests for mathematical operations")
Stream<DynamicTest> dynamicTestsForMathOperations() {
Calculator calculator = new Calculator();
List<MathOperation> operations = Arrays.asList(
new MathOperation(2, 3, 5, "addition"),
new MathOperation(5, 3, 2, "subtraction"),
new MathOperation(4, 5, 20, "multiplication"),
new MathOperation(10, 2, 5, "division")
);
return operations.stream()
.map(operation -> dynamicTest(
operation.getDescription(),
() -> {
int result = switch (operation.getType()) {
case "addition" -> calculator.add(operation.getA(), operation.getB());
case "subtraction" -> calculator.subtract(operation.getA(), operation.getB());
case "multiplication" -> calculator.multiply(operation.getA(), operation.getB());
case "division" -> calculator.divide(operation.getA(), operation.getB());
default -> throw new IllegalArgumentException("Unknown operation: " + operation.getType());
};
assertEquals(operation.getExpected(), result);
}
));
}
@TestFactory
@DisplayName("Dynamic test containers")
Stream<DynamicNode> dynamicTestsWithContainers() {
return Stream.of(
dynamicContainer("Math Operations",
Stream.of(
dynamicTest("Addition", () -> assertEquals(4, 2 + 2)),
dynamicTest("Multiplication", () -> assertEquals(6, 2 * 3))
)),
dynamicContainer("String Operations",
Stream.of(
dynamicTest("Length", () -> assertEquals(5, "Hello".length())),
dynamicTest("Substring", () -> assertEquals("ell", "Hello".substring(1, 4)))
))
);
}
@TestFactory
@DisplayName("Dynamic tests with data from external source")
Stream<DynamicTest> dynamicTestsFromExternalSource() {
// Simulating external data source
List<TestData> testData = Arrays.asList(
new TestData("Basic addition", () -> assertEquals(2, 1 + 1)),
new TestData("String concatenation", () -> assertEquals("Hello World", "Hello" + " " + "World")),
new TestData("Array length", () -> assertEquals(3, new int[]{1, 2, 3}.length))
);
return testData.stream()
.map(data -> dynamicTest(data.getDisplayName(), data.getExecutable()));
}
// Helper classes
static class MathOperation {
private int a;
private int b;
private int expected;
private String type;
public MathOperation(int a, int b, int expected, String type) {
this.a = a;
this.b = b;
this.expected = expected;
this.type = type;
}
public int getA() { return a; }
public int getB() { return b; }
public int getExpected() { return expected; }
public String getType() { return type; }
public String getDescription() { return type + ": " + a + " and " + b + " should equal " + expected; }
}
static class TestData {
private String displayName;
private Executable executable;
public TestData(String displayName, Executable executable) {
this.displayName = displayName;
this.executable = executable;
}
public String getDisplayName() { return displayName; }
public Executable getExecutable() { return executable; }
}
}

Testing Best Practices

1. Test Structure and Organization

// BestPracticesTest.java
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Testing Best Practices Examples")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class BestPracticesTest {
private FileProcessor fileProcessor;
@BeforeAll
void setUpClass() {
// Initialize expensive resources once
fileProcessor = new FileProcessor();
}
@BeforeEach
void setUp() {
// Reset state before each test
fileProcessor.clearCache();
}
@Test
@DisplayName("✅ Should process valid file content")
void processFile_WithValidContent_ShouldSucceed() {
// Arrange
String content = "Hello, World!";
// Act
String result = fileProcessor.processContent(content);
// Assert
assertEquals("Processed: Hello, World!", result);
}
@Test
@DisplayName("❌ Should throw exception for null content")
void processFile_WithNullContent_ShouldThrowException() {
// Arrange
String content = null;
// Act & Assert
assertThrows(IllegalArgumentException.class, 
() -> fileProcessor.processContent(content));
}
@Test
@DisplayName("📊 Should handle large files efficiently")
void processFile_WithLargeContent_ShouldHandleEfficiently() {
// Arrange
String largeContent = "A".repeat(10000);
// Act
String result = fileProcessor.processContent(largeContent);
// Assert
assertAll("large content processing",
() -> assertNotNull(result),
() -> assertTrue(result.startsWith("Processed:")),
() -> assertEquals(10010 + "A".length(), result.length())
);
}
@Test
@DisplayName("💾 Should create file in temporary directory")
void createFile_WithTemporaryDirectory_ShouldSucceed(@TempDir Path tempDir) throws IOException {
// Arrange
Path testFile = tempDir.resolve("test.txt");
String content = "Test content";
// Act
Files.writeString(testFile, content);
// Assert
assertAll("file creation",
() -> assertTrue(Files.exists(testFile)),
() -> assertEquals(content, Files.readString(testFile))
);
}
@Test
@DisplayName("🔍 Should find all matching patterns")
void findPatterns_WithMultipleMatches_ShouldReturnAll() {
// Arrange
String content = "abc def abc ghi abc";
String pattern = "abc";
// Act
List<String> matches = fileProcessor.findPatterns(content, pattern);
// Assert
assertAll("pattern matching",
() -> assertEquals(3, matches.size()),
() -> assertTrue(matches.stream().allMatch("abc"::equals))
);
}
@Test
@DisplayName("⚡ Should complete within time limit")
void processFile_WithTimeout_ShouldCompleteQuickly() {
// Arrange
String content = "Quick processing";
// Act & Assert
assertTimeoutPreemptively(java.time.Duration.ofSeconds(1), 
() -> fileProcessor.processContent(content));
}
@Test
@DisplayName("🔄 Should maintain state between operations")
void multipleOperations_ShouldMaintainConsistentState() {
// Arrange
String content1 = "First";
String content2 = "Second";
// Act
fileProcessor.processContent(content1);
fileProcessor.processContent(content2);
int processedCount = fileProcessor.getProcessedCount();
// Assert
assertEquals(2, processedCount);
}
@AfterEach
void tearDown() {
// Clean up after each test
fileProcessor.resetCounters();
}
@AfterAll
void tearDownClass() {
// Clean up expensive resources
fileProcessor.shutdown();
}
}
// FileProcessor.java (Class under test)
class FileProcessor {
private int processedCount = 0;
private String cache;
public String processContent(String content) {
if (content == null) {
throw new IllegalArgumentException("Content cannot be null");
}
processedCount++;
cache = content;
return "Processed: " + content;
}
public List<String> findPatterns(String content, String pattern) {
// Simple implementation for demonstration
return List.of(content.split(" ")).stream()
.filter(word -> word.equals(pattern))
.toList();
}
public void clearCache() {
cache = null;
}
public void resetCounters() {
processedCount = 0;
}
public int getProcessedCount() {
return processedCount;
}
public void shutdown() {
// Cleanup resources
}
}

2. Test Naming and Organization

// TestNamingConventionsTest.java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Test naming conventions:
* - MethodName_StateUnderTest_ExpectedBehavior
* - MethodName_ExpectedBehavior_WhenState
* - Should_ExpectedBehavior_When_State
*/
class TestNamingConventionsTest {
private BankAccount bankAccount;
@BeforeEach
void setUp() {
bankAccount = new BankAccount(100.0);
}
// Convention: MethodName_StateUnderTest_ExpectedBehavior
@Test
@DisplayName("withdraw_WithSufficientBalance_ShouldDecreaseBalance")
void withdraw_WithSufficientBalance_ShouldDecreaseBalance() {
// Act
bankAccount.withdraw(50.0);
// Assert
assertEquals(50.0, bankAccount.getBalance());
}
@Test
@DisplayName("withdraw_WithInsufficientBalance_ShouldThrowException")
void withdraw_WithInsufficientBalance_ShouldThrowException() {
// Act & Assert
assertThrows(InsufficientFundsException.class, 
() -> bankAccount.withdraw(150.0));
}
// Convention: MethodName_ExpectedBehavior_WhenState
@Test
@DisplayName("deposit_ShouldIncreaseBalance_WhenPositiveAmount")
void deposit_ShouldIncreaseBalance_WhenPositiveAmount() {
// Act
bankAccount.deposit(50.0);
// Assert
assertEquals(150.0, bankAccount.getBalance());
}
@Test
@DisplayName("deposit_ShouldThrowException_WhenNegativeAmount")
void deposit_ShouldThrowException_WhenNegativeAmount() {
// Act & Assert
assertThrows(IllegalArgumentException.class, 
() -> bankAccount.deposit(-50.0));
}
// Convention: Should_ExpectedBehavior_When_State
@Test
@DisplayName("Should_TransferAmount_When_BothAccountsValid")
void should_TransferAmount_When_BothAccountsValid() {
// Arrange
BankAccount sourceAccount = new BankAccount(100.0);
BankAccount targetAccount = new BankAccount(50.0);
// Act
sourceAccount.transfer(30.0, targetAccount);
// Assert
assertAll("transfer",
() -> assertEquals(70.0, sourceAccount.getBalance()),
() -> assertEquals(80.0, targetAccount.getBalance())
);
}
@Test
@DisplayName("Should_ThrowException_When_TransferExceedsBalance")
void should_ThrowException_When_TransferExceedsBalance() {
// Arrange
BankAccount sourceAccount = new BankAccount(100.0);
BankAccount targetAccount = new BankAccount(50.0);
// Act & Assert
assertThrows(InsufficientFundsException.class,
() -> sourceAccount.transfer(150.0, targetAccount));
}
// BDD-style naming
@Nested
@DisplayName("Given a bank account with initial balance")
class GivenBankAccountWithInitialBalance {
@BeforeEach
void setUp() {
bankAccount = new BankAccount(100.0);
}
@Nested
@DisplayName("When withdrawing valid amount")
class WhenWithdrawingValidAmount {
@Test
@DisplayName("Then balance should decrease")
void thenBalanceShouldDecrease() {
// Act
bankAccount.withdraw(30.0);
// Assert
assertEquals(70.0, bankAccount.getBalance());
}
}
@Nested
@DisplayName("When withdrawing excessive amount")
class WhenWithdrawingExcessiveAmount {
@Test
@DisplayName("Then should throw insufficient funds exception")
void thenShouldThrowInsufficientFundsException() {
// Act & Assert
assertThrows(InsufficientFundsException.class,
() -> bankAccount.withdraw(150.0));
}
}
}
}
// Supporting classes
class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void withdraw(double amount) {
if (amount > balance) {
throw new InsufficientFundsException("Insufficient funds");
}
balance -= amount;
}
public void deposit(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Deposit amount cannot be negative");
}
balance += amount;
}
public void transfer(double amount, BankAccount targetAccount) {
if (amount > balance) {
throw new InsufficientFundsException("Insufficient funds for transfer");
}
this.withdraw(amount);
targetAccount.deposit(amount);
}
public double getBalance() {
return balance;
}
}
class InsufficientFundsException extends RuntimeException {
public InsufficientFundsException(String message) {
super(message);
}
}

Integration with Spring Boot

1. Spring Boot Test Configuration

// SpringBootTestExamples.java
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.EnabledIf;
import java.util.Optional;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@DisplayName("Spring Boot Integration Tests")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SpringBootTestExamples {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@MockBean
private EmailService emailService;
private User testUser;
@BeforeAll
void setUpClass() {
testUser = new User(1L, "[email protected]", "John", "Doe", true);
}
@BeforeEach
void setUp() {
reset(userRepository, emailService);
}
@Test
@DisplayName("Should save user when valid data provided")
void createUser_WithValidData_ShouldSaveUser() {
// Arrange
when(userRepository.save(any(User.class))).thenReturn(testUser);
doNothing().when(emailService).sendWelcomeEmail(anyString());
// Act
User createdUser = userService.createUser("[email protected]", "John", "Doe");
// Assert
assertAll("user creation",
() -> assertNotNull(createdUser),
() -> assertEquals("[email protected]", createdUser.getEmail()),
() -> verify(userRepository, times(1)).save(any(User.class)),
() -> verify(emailService, times(1)).sendWelcomeEmail("[email protected]")
);
}
@Test
@DisplayName("Should find user by existing ID")
void findUserById_WithExistingId_ShouldReturnUser() {
// Arrange
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
// Act
User foundUser = userService.findUserById(1L);
// Assert
assertAll("user retrieval",
() -> assertNotNull(foundUser),
() -> assertEquals(1L, foundUser.getId()),
() -> assertEquals("[email protected]", foundUser.getEmail())
);
}
@Test
@DisplayName("Should return null for non-existing user ID")
void findUserById_WithNonExistingId_ShouldReturnNull() {
// Arrange
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// Act
User foundUser = userService.findUserById(999L);
// Assert
assertNull(foundUser);
verify(userRepository, times(1)).findById(999L);
}
@Test
@DisplayName("Should deactivate user and send notification")
void deactivateUser_WithValidId_ShouldDeactivateAndNotify() {
// Arrange
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
when(userRepository.save(any(User.class))).thenReturn(testUser);
doNothing().when(emailService).sendDeactivationEmail(anyString());
// Act
userService.deactivateUser(1L);
// Assert
verify(userRepository, times(1)).findById(1L);
verify(userRepository, times(1)).save(any(User.class));
verify(emailService, times(1)).sendDeactivationEmail("[email protected]");
}
@Test
@EnabledIf(expression = "${spring.datasource.url:false}", loadContext = true)
@DisplayName("Should run only when database is available")
void databaseDependentTest() {
// This test only runs when database URL is configured
assertTrue(userService != null);
}
@AfterEach
void tearDown() {
// Ensure no more interactions with mocks
verifyNoMoreInteractions(userRepository, emailService);
}
}
// Spring Boot components (simplified)
// @Service
class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User createUser(String email, String firstName, String lastName) {
User user = new User(null, email, firstName, lastName, true);
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(email);
return savedUser;
}
public User findUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
public void deactivateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
user.setActive(false);
userRepository.save(user);
emailService.sendDeactivationEmail(user.getEmail());
}
}
// @Repository
interface UserRepository {
User save(User user);
Optional<User> findById(Long id);
}
// @Service
class EmailService {
public void sendWelcomeEmail(String email) {
// Implementation
}
public void sendDeactivationEmail(String email) {
// Implementation
}
}

Conclusion

JUnit 5 provides a modern, flexible testing framework for Java applications. Key takeaways:

  1. Use descriptive test names with @DisplayName for better readability
  2. Organize tests logically with nested classes and proper lifecycle management
  3. Leverage parameterized tests for data-driven testing scenarios
  4. Use appropriate assertions and consider AssertJ for fluent assertions
  5. Implement test interfaces for reusable test behavior
  6. Use dynamic tests for programmatic test generation
  7. Follow best practices for test structure, naming, and organization
  8. Integrate properly with Spring Boot using appropriate test annotations

By following these patterns and best practices, you can create maintainable, reliable, and comprehensive test suites that effectively verify your application's behavior.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper