JUnit 5 Testing Framework

Comprehensive JUnit 5 testing examples including unit tests, integration tests, parameterized tests, mocking with Mockito, and advanced testing patterns for Java applications

Key Facts

Category
Testing
Items
2
Format Families
sample

Sample Overview

Comprehensive JUnit 5 testing examples including unit tests, integration tests, parameterized tests, mocking with Mockito, and advanced testing patterns for Java applications This sample set belongs to Testing and can be used to test related workflows inside Elysia Tools.

💻 JUnit 5 Basic Setup and Configuration java

🟢 simple

Complete JUnit 5 project setup with Maven/Gradle configuration, basic test structure, and common annotations

⏱️ 20 min 🏷️ junit5, setup, maven, gradle
Prerequisites: Java basics, Maven/Gradle, Testing concepts
// JUnit 5 Configuration Examples

// 1. Maven Configuration - pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>java-testing-example</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- JUnit 5 version -->
        <junit.version>5.10.1</junit.version>
        <!-- Mockito version -->
        <mockito.version>5.7.0</mockito.version>
        <!-- AssertJ version -->
        <assertj.version>3.24.2</assertj.version>
    </properties>

    <dependencies>
        <!-- JUnit 5 Jupiter -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- JUnit 5 Parameterized Tests -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- JUnit 5 Suite Support -->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite</artifactId>
            <version>1.10.1</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>

        <!-- Mockito with JUnit 5 -->
        <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>

        <!-- Testcontainers for integration testing -->
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>1.19.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Maven Compiler Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>

            <!-- Maven Surefire Plugin for running tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <!-- Include only JUnit 5 tests -->
                    <includes>
                        <include>**/*Test.java</include>
                        <include>**/*Tests.java</include>
                    </includes>

                    <!-- Test configuration -->
                    <systemPropertyVariables>
                        <java.util.logging.config.file>src/test/resources/logging.properties</java.util.logging.config.file>
                    </systemPropertyVariables>

                    <!-- Parallel test execution -->
                    <parallel>methods</parallel>
                    <threadCount>4</threadCount>
                </configuration>
            </plugin>

            <!-- JaCoCo for code coverage -->
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.11</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

// 2. Gradle Configuration - build.gradle.kts
plugins {
    java
    jacoco
    application
}

group = "com.example"
version = "1.0.0"

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

repositories {
    mavenCentral()
}

dependencies {
    // JUnit 5
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
    testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.1")
    testImplementation("org.junit.platform:junit-platform-suite:1.10.1")

    // Mockito
    testImplementation("org.mockito:mockito-core:5.7.0")
    testImplementation("org.mockito:mockito-junit-jupiter:5.7.0")

    // AssertJ
    testImplementation("org.assertj:assertj-core:3.24.2")

    // Testcontainers
    testImplementation("org.testcontainers:junit-jupiter:1.19.3")
}

tasks.test {
    useJUnitPlatform()

    // Configure test execution
    systemProperties("java.util.logging.config.file" to "src/test/resources/logging.properties")

    // Parallel execution
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1

    // Test logging
    testLogging {
        events("passed", "skipped", "failed")
        exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
    }
}

// JaCoCo configuration
jacoco {
    toolVersion = "0.8.11"
}

tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        xml.required.set(true)
        html.required.set(true)
    }
}

// 3. JUnit 5 Configuration - src/test/resources/junit-platform.properties
# JUnit Platform Configuration

# Enable parallel execution
junit.jupiter.execution.parallel.enabled = true

# Parallel execution mode: same_thread, concurrent
junit.jupiter.execution.parallel.mode.default = concurrent

# Class-level parallel mode: same_thread, concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent

# Test instance lifecycle: per_class, per_method
junit.jupiter.testinstance.lifecycle.default = per_class

# Display test execution
junit.jupiter.extensions.autodetection.enabled = true

# Condition execution
junit.jupiter.conditions.deactivate = org.junit.*DisabledCondition

# 4. src/test/resources/logback-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>target/test.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

// 5. src/main/java/com/example/calculator/Calculator.java
package com.example.calculator;

public class Calculator {

    public double add(double a, double b) {
        return a + b;
    }

    public double subtract(double a, double b) {
        return a - b;
    }

    public double multiply(double a, double b) {
        return a * b;
    }

    public double divide(double a, double b) {
        if (b == 0) {
            throw new IllegalArgumentException("Cannot divide by zero");
        }
        return a / b;
    }

    public double power(double base, double exponent) {
        return Math.pow(base, exponent);
    }

    public double sqrt(double number) {
        if (number < 0) {
            throw new IllegalArgumentException("Cannot calculate square root of negative number");
        }
        return Math.sqrt(number);
    }
}

// 6. src/test/java/com/example/calculator/CalculatorTest.java
package com.example.calculator;

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;

class CalculatorTest {

    private Calculator calculator;

    // @BeforeEach - Run before each test
    @BeforeEach
    void setUp() {
        calculator = new Calculator();
        System.out.println("Setting up Calculator instance");
    }

    // @AfterEach - Run after each test
    @AfterEach
    void tearDown() {
        calculator = null;
        System.out.println("Cleaning up Calculator instance");
    }

    // @BeforeAll - Run once before all tests
    @BeforeAll
    static void initAll() {
        System.out.println("Initializing all tests");
    }

    // @AfterAll - Run once after all tests
    @AfterAll
    static void tearDownAll() {
        System.out.println("Cleaning up all tests");
    }

    // Basic test with JUnit 5 assertions
    @Test
    @DisplayName("Should add two numbers correctly")
    @Tag("basic")
    void shouldAddTwoNumbers() {
        // Given
        double a = 5.0;
        double b = 3.0;

        // When
        double result = calculator.add(a, b);

        // Then
        assertEquals(8.0, result, "5 + 3 should equal 8");
        assertThat(result).isEqualTo(8.0);
    }

    @Test
    @DisplayName("Should subtract two numbers correctly")
    @Tag("basic")
    void shouldSubtractTwoNumbers() {
        // Given
        double a = 10.0;
        double b = 4.0;

        // When
        double result = calculator.subtract(a, b);

        // Then
        assertEquals(6.0, result, "10 - 4 should equal 6");
        assertThat(result).isPositive().isGreaterThan(5.0);
    }

    @Test
    @DisplayName("Should multiply two numbers correctly")
    @Tag("basic")
    void shouldMultiplyTwoNumbers() {
        // Given
        double a = 4.0;
        double b = 3.0;

        // When
        double result = calculator.multiply(a, b);

        // Then
        assertEquals(12.0, result, "4 * 3 should equal 12");
        assertThat(result).isEqualTo(12.0);
    }

    @Test
    @DisplayName("Should divide two numbers correctly")
    @Tag("basic")
    void shouldDivideTwoNumbers() {
        // Given
        double a = 15.0;
        double b = 3.0;

        // When
        double result = calculator.divide(a, b);

        // Then
        assertEquals(5.0, result, 0.001, "15 / 3 should equal 5");
        assertThat(result).isBetween(4.9, 5.1);
    }

    @Test
    @DisplayName("Should throw exception when dividing by zero")
    @Tag("exception")
    void shouldThrowExceptionWhenDividingByZero() {
        // Given
        double a = 10.0;
        double b = 0.0;

        // When & Then
        IllegalArgumentException exception = assertThrows(
            IllegalArgumentException.class,
            () -> calculator.divide(a, b),
            "Should throw IllegalArgumentException"
        );

        assertEquals("Cannot divide by zero", exception.getMessage());

        // Alternative with AssertJ
        assertThatThrownBy(() -> calculator.divide(a, b))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("Cannot divide by zero");
    }

    // Parameterized test with @ValueSource
    @ParameterizedTest
    @ValueSource(doubles = {0.0, 1.0, 4.0, 9.0, 16.0, 25.0})
    @DisplayName("Should calculate square root correctly")
    @Tag("parameterized")
    void shouldCalculateSquareRootCorrectly(double number) {
        // When
        double result = calculator.sqrt(number);

        // Then
        assertThat(result).isGreaterThanOrEqualTo(0.0);
        assertThat(result * result).isCloseTo(number, within(0.001));
    }

    @ParameterizedTest
    @ValueSource(doubles = {-1.0, -4.0, -9.0})
    @DisplayName("Should throw exception for negative square root")
    @Tag("parameterized")
    void shouldThrowExceptionForNegativeSquareRoot(double number) {
        assertThatThrownBy(() -> calculator.sqrt(number))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("Cannot calculate square root of negative number");
    }

    // Parameterized test with @CsvSource
    @ParameterizedTest(name = "Test {index}: {0} ^ {1} = {2}")
    @CsvSource({
        "2.0, 3.0, 8.0",
        "3.0, 2.0, 9.0",
        "5.0, 0.0, 1.0",
        "10.0, 1.0, 10.0"
    })
    @DisplayName("Should calculate power correctly")
    @Tag("parameterized")
    void shouldCalculatePowerCorrectly(double base, double exponent, double expected) {
        // When
        double result = calculator.power(base, exponent);

        // Then
        assertThat(result).isCloseTo(expected, within(0.001));
    }

    // Parameterized test with @MethodSource
    @ParameterizedTest
    @MethodSource("provideMultiplicationData")
    @DisplayName("Should multiply with various inputs")
    @Tag("parameterized")
    void shouldMultiplyWithVariousInputs(double a, double b, double expected) {
        // When
        double result = calculator.multiply(a, b);

        // Then
        assertThat(result).isCloseTo(expected, within(0.001));
    }

    static Stream<Arguments> provideMultiplicationData() {
        return Stream.of(
            Arguments.of(2.5, 4.0, 10.0),
            Arguments.of(-3.0, 2.0, -6.0),
            Arguments.of(0.0, 5.0, 0.0),
            Arguments.of(1.5, 1.5, 2.25)
        );
    }

    // Nested test class
    @Nested
    @DisplayName("Edge Case Tests")
    @Tag("edge-case")
    class EdgeCaseTests {

        @Test
        @DisplayName("Should handle very large numbers")
        void shouldHandleVeryLargeNumbers() {
            // Given
            double largeNumber = Double.MAX_VALUE / 2;

            // When
            double result = calculator.add(largeNumber, 1.0);

            // Then
            assertThat(result).isGreaterThan(largeNumber);
            assertThat(Double.isInfinite(result)).isFalse();
        }

        @Test
        @DisplayName("Should handle floating point precision")
        void shouldHandleFloatingPointPrecision() {
            // Given
            double a = 0.1;
            double b = 0.2;

            // When
            double result = calculator.add(a, b);

            // Then
            assertThat(result).isCloseTo(0.3, within(0.001));
        }
    }

    // Disabled test
    @Test
    @Disabled("This test is disabled for demonstration")
    @DisplayName("Disabled test example")
    void disabledTestExample() {
        fail("This test should not run");
    }

    // Repeated test
    @RepeatedTest(3)
    @DisplayName("Repeated test example")
    void repeatedTestExample(RepetitionInfo repetitionInfo) {
        System.out.println("Running repetition " + repetitionInfo.getCurrentRepetition());
        assertTrue(calculator.add(1, 1) == 2);
    }

    // Test timeout
    @Test
    @Timeout(1) // 1 second timeout
    @DisplayName("Should complete within timeout")
    void shouldCompleteWithinTimeout() throws InterruptedException {
        // This test should complete within 1 second
        Thread.sleep(100);
        assertTrue(calculator.add(1, 1) == 2);
    }

    // Custom assumptions
    @Test
    @DisplayName("Should run only on certain conditions")
    void shouldRunOnlyOnCertainConditions() {
        // Skip test if running on Windows
        Assumptions.assumeFalse(System.getProperty("os.name").toLowerCase().contains("win"),
            "Test skipped on Windows");

        // Continue with test
        assertThat(calculator.add(2, 3)).isEqualTo(5);
    }
}

💻 JUnit 5 with Mockito and Testcontainers java

🔴 complex ⭐⭐⭐⭐

Advanced testing patterns using Mockito for mocking and Testcontainers for integration testing

⏱️ 45 min 🏷️ junit5, mockito, testcontainers, integration
Prerequisites: Java advanced, Spring Boot, Database concepts, Testing patterns
// JUnit 5 Advanced Testing with Mockito and Testcontainers

// 1. src/main/java/com/example/service/UserService.java
package com.example.service;

import com.example.model.User;
import com.example.repository.UserRepository;
import com.example.exception.UserNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Service
@Transactional
public 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(User user) {
        user.setCreatedAt(LocalDateTime.now());
        user.setActive(true);

        User savedUser = userRepository.save(user);

        // Send welcome email
        emailService.sendWelcomeEmail(savedUser.getEmail(), savedUser.getUsername());

        return savedUser;
    }

    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }

    public List<User> getAllActiveUsers() {
        return userRepository.findByActiveTrue();
    }

    public User updateUser(Long id, User userDetails) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));

        user.setUsername(userDetails.getUsername());
        user.setEmail(userDetails.getEmail());
        user.setUpdatedAt(LocalDateTime.now());

        return userRepository.save(user);
    }

    public void deleteUser(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));

        userRepository.delete(user);
    }

    public User deactivateUser(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));

        user.setActive(false);
        user.setDeactivatedAt(LocalDateTime.now());

        return userRepository.save(user);
    }
}

// 2. src/main/java/com/example/service/EmailService.java
package com.example.service;

import org.springframework.stereotype.Service;

@Service
public class EmailService {

    public void sendWelcomeEmail(String email, String username) {
        // Implementation would send actual email
        System.out.println("Sending welcome email to: " + email);
    }

    public void sendPasswordResetEmail(String email, String resetToken) {
        // Implementation would send password reset email
        System.out.println("Sending password reset email to: " + email);
    }
}

// 3. src/main/java/com/example/model/User.java
package com.example.model;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private Boolean active = true;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @Column(name = "deactivated_at")
    private LocalDateTime deactivatedAt;

    // Constructors
    public User() {}

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    public Boolean getActive() { return active; }
    public void setActive(Boolean active) { this.active = active; }

    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }

    public LocalDateTime getUpdatedAt() { return updatedAt; }
    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }

    public LocalDateTime getDeactivatedAt() { return deactivatedAt; }
    public void setDeactivatedAt(LocalDateTime deactivatedAt) { this.deactivatedAt = deactivatedAt; }
}

// 4. src/test/java/com/example/service/UserServiceMockitoTest.java
package com.example.service;

import com.example.model.User;
import com.example.repository.UserRepository;
import com.example.exception.UserNotFoundException;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("UserService Tests with Mockito")
class UserServiceMockitoTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;

    @Captor
    private ArgumentCaptor<User> userCaptor;

    @Test
    @DisplayName("Should create user successfully")
    void shouldCreateUserSuccessfully() {
        // Given
        User userToCreate = new User("testuser", "[email protected]", "password123");
        User savedUser = new User(1L, "testuser", "[email protected]", "password123");
        savedUser.setCreatedAt(LocalDateTime.now());
        savedUser.setActive(true);

        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        // When
        User result = userService.createUser(userToCreate);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getUsername()).isEqualTo("testuser");
        assertThat(result.getEmail()).isEqualTo("[email protected]");
        assertThat(result.getActive()).isTrue();
        assertThat(result.getCreatedAt()).isNotNull();

        // Verify interactions
        verify(userRepository, times(1)).save(userCaptor.capture());
        verify(emailService, times(1)).sendWelcomeEmail("[email protected]", "testuser");

        // Verify captured user
        User capturedUser = userCaptor.getValue();
        assertThat(capturedUser.getUsername()).isEqualTo("testuser");
        assertThat(capturedUser.getActive()).isTrue();
    }

    @Test
    @DisplayName("Should get user by ID when user exists")
    void shouldGetUserByIdWhenUserExists() {
        // Given
        User user = new User(1L, "testuser", "[email protected]", "password123");
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        // When
        Optional<User> result = userService.getUserById(1L);

        // Then
        assertThat(result).isPresent();
        assertThat(result.get().getId()).isEqualTo(1L);
        assertThat(result.get().getUsername()).isEqualTo("testuser");

        verify(userRepository, times(1)).findById(1L);
    }

    @Test
    @DisplayName("Should return empty when user does not exist")
    void shouldReturnEmptyWhenUserDoesNotExist() {
        // Given
        when(userRepository.findById(999L)).thenReturn(Optional.empty());

        // When
        Optional<User> result = userService.getUserById(999L);

        // Then
        assertThat(result).isEmpty();
        verify(userRepository, times(1)).findById(999L);
    }

    @Test
    @DisplayName("Should get all active users")
    void shouldGetAllActiveUsers() {
        // Given
        List<User> activeUsers = Arrays.asList(
            new User(1L, "user1", "[email protected]", "pass1"),
            new User(2L, "user2", "[email protected]", "pass2")
        );
        when(userRepository.findByActiveTrue()).thenReturn(activeUsers);

        // When
        List<User> result = userService.getAllActiveUsers();

        // Then
        assertThat(result).hasSize(2);
        assertThat(result.get(0).getUsername()).isEqualTo("user1");
        assertThat(result.get(1).getUsername()).isEqualTo("user2");

        verify(userRepository, times(1)).findByActiveTrue();
    }

    @Test
    @DisplayName("Should update user successfully")
    void shouldUpdateUserSuccessfully() {
        // Given
        User existingUser = new User(1L, "olduser", "[email protected]", "password");
        User userDetails = new User("newuser", "[email protected]", "newpassword");

        when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser));
        when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));

        // When
        User result = userService.updateUser(1L, userDetails);

        // Then
        assertThat(result.getUsername()).isEqualTo("newuser");
        assertThat(result.getEmail()).isEqualTo("[email protected]");
        assertThat(result.getUpdatedAt()).isNotNull();

        verify(userRepository, times(1)).findById(1L);
        verify(userRepository, times(1)).save(any(User.class));
    }

    @Test
    @DisplayName("Should throw exception when updating non-existent user")
    void shouldThrowExceptionWhenUpdatingNonExistentUser() {
        // Given
        User userDetails = new User("newuser", "[email protected]", "newpassword");
        when(userRepository.findById(999L)).thenReturn(Optional.empty());

        // When & Then
        UserNotFoundException exception = assertThrows(
            UserNotFoundException.class,
            () -> userService.updateUser(999L, userDetails)
        );

        assertThat(exception.getMessage()).isEqualTo("User not found with id: 999");
        verify(userRepository, times(1)).findById(999L);
        verify(userRepository, never()).save(any(User.class));
    }

    @Test
    @DisplayName("Should delete user successfully")
    void shouldDeleteUserSuccessfully() {
        // Given
        User user = new User(1L, "testuser", "[email protected]", "password");
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        doNothing().when(userRepository).delete(user);

        // When
        userService.deleteUser(1L);

        // Then
        verify(userRepository, times(1)).findById(1L);
        verify(userRepository, times(1)).delete(user);
    }

    @Test
    @DisplayName("Should deactivate user successfully")
    void shouldDeactivateUserSuccessfully() {
        // Given
        User user = new User(1L, "testuser", "[email protected]", "password");
        user.setActive(true);
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));

        // When
        User result = userService.deactivateUser(1L);

        // Then
        assertThat(result.getActive()).isFalse();
        assertThat(result.getDeactivatedAt()).isNotNull();

        verify(userRepository, times(1)).findById(1L);
        verify(userRepository, times(1)).save(user);
    }

    @Test
    @DisplayName("Should throw exception when deactivating non-existent user")
    void shouldThrowExceptionWhenDeactivatingNonExistentUser() {
        // Given
        when(userRepository.findById(999L)).thenReturn(Optional.empty());

        // When & Then
        UserNotFoundException exception = assertThrows(
            UserNotFoundException.class,
            () -> userService.deactivateUser(999L)
        );

        assertThat(exception.getMessage()).isEqualTo("User not found with id: 999");
        verify(userRepository, times(1)).findById(999L);
        verify(userRepository, never()).save(any(User.class));
    }

    // Test with multiple verifications
    @Test
    @DisplayName("Should create multiple users and verify all interactions")
    void shouldCreateMultipleUsersAndVerifyAllInteractions() {
        // Given
        User user1 = new User("user1", "[email protected]", "password1");
        User user2 = new User("user2", "[email protected]", "password2");

        User savedUser1 = new User(1L, "user1", "[email protected]", "password1");
        User savedUser2 = new User(2L, "user2", "[email protected]", "password2");

        when(userRepository.save(any(User.class)))
            .thenReturn(savedUser1)
            .thenReturn(savedUser2);

        // When
        userService.createUser(user1);
        userService.createUser(user2);

        // Then
        verify(userRepository, times(2)).save(any(User.class));
        verify(emailService, times(1)).sendWelcomeEmail("[email protected]", "user1");
        verify(emailService, times(1)).sendWelcomeEmail("[email protected]", "user2");

        // Verify order of interactions
        InOrder inOrder = inOrder(userRepository, emailService);
        inOrder.verify(userRepository).save(any(User.class));
        inOrder.verify(emailService).sendWelcomeEmail("[email protected]", "user1");
        inOrder.verify(userRepository).save(any(User.class));
        inOrder.verify(emailService).sendWelcomeEmail("[email protected]", "user2");
    }
}

// 5. src/test/java/com/example/repository/UserRepositoryIntegrationTest.java
package com.example.repository;

import com.example.model.User;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.*;

@DataJpaTest
@Testcontainers
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("UserRepository Integration Tests")
class UserRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    private User testUser;

    @BeforeEach
    void setUp() {
        testUser = new User("testuser", "[email protected]", "password123");
        testUser.setCreatedAt(LocalDateTime.now());
        testUser.setActive(true);
    }

    @Test
    @Order(1)
    @DisplayName("Should save user successfully")
    void shouldSaveUserSuccessfully() {
        // When
        User savedUser = userRepository.save(testUser);

        // Then
        assertThat(savedUser).isNotNull();
        assertThat(savedUser.getId()).isNotNull();
        assertThat(savedUser.getUsername()).isEqualTo("testuser");
        assertThat(savedUser.getEmail()).isEqualTo("[email protected]");
        assertThat(savedUser.getActive()).isTrue();
    }

    @Test
    @Order(2)
    @DisplayName("Should find user by ID")
    void shouldFindUserById() {
        // Given
        User savedUser = userRepository.save(testUser);

        // When
        Optional<User> foundUser = userRepository.findById(savedUser.getId());

        // Then
        assertThat(foundUser).isPresent();
        assertThat(foundUser.get().getId()).isEqualTo(savedUser.getId());
        assertThat(foundUser.get().getUsername()).isEqualTo("testuser");
    }

    @Test
    @Order(3)
    @DisplayName("Should find user by email")
    void shouldFindUserByEmail() {
        // Given
        userRepository.save(testUser);

        // When
        Optional<User> foundUser = userRepository.findByEmail("[email protected]");

        // Then
        assertThat(foundUser).isPresent();
        assertThat(foundUser.get().getEmail()).isEqualTo("[email protected]");
        assertThat(foundUser.get().getUsername()).isEqualTo("testuser");
    }

    @Test
    @Order(4)
    @DisplayName("Should find user by username")
    void shouldFindUserByUsername() {
        // Given
        userRepository.save(testUser);

        // When
        Optional<User> foundUser = userRepository.findByUsername("testuser");

        // Then
        assertThat(foundUser).isPresent();
        assertThat(foundUser.get().getUsername()).isEqualTo("testuser");
        assertThat(foundUser.get().getEmail()).isEqualTo("[email protected]");
    }

    @Test
    @Order(5)
    @DisplayName("Should find all active users")
    void shouldFindAllActiveUsers() {
        // Given
        User activeUser = new User("activeuser", "[email protected]", "password");
        activeUser.setActive(true);
        activeUser.setCreatedAt(LocalDateTime.now());

        User inactiveUser = new User("inactiveuser", "[email protected]", "password");
        inactiveUser.setActive(false);
        inactiveUser.setCreatedAt(LocalDateTime.now());

        userRepository.save(activeUser);
        userRepository.save(inactiveUser);
        userRepository.save(testUser);

        // When
        List<User> activeUsers = userRepository.findByActiveTrue();

        // Then
        assertThat(activeUsers).hasSize(2);
        assertThat(activeUsers.stream().allMatch(User::getActive)).isTrue();
        assertThat(activeUsers).noneMatch(user -> "inactiveuser".equals(user.getUsername()));
    }

    @Test
    @Order(6)
    @DisplayName("Should check if email exists")
    void shouldCheckIfEmailExists() {
        // Given
        userRepository.save(testUser);

        // When & Then
        assertThat(userRepository.existsByEmail("[email protected]")).isTrue();
        assertThat(userRepository.existsByEmail("[email protected]")).isFalse();
    }

    @Test
    @Order(7)
    @DisplayName("Should check if username exists")
    void shouldCheckIfUsernameExists() {
        // Given
        userRepository.save(testUser);

        // When & Then
        assertThat(userRepository.existsByUsername("testuser")).isTrue();
        assertThat(userRepository.existsByUsername("nonexistent")).isFalse();
    }

    @Test
    @Order(8)
    @DisplayName("Should delete user by ID")
    void shouldDeleteUserById() {
        // Given
        User savedUser = userRepository.save(testUser);
        Long userId = savedUser.getId();

        // When
        userRepository.deleteById(userId);

        // Then
        Optional<User> deletedUser = userRepository.findById(userId);
        assertThat(deletedUser).isEmpty();
    }

    @Test
    @DisplayName("Should handle unique constraint violation")
    void shouldHandleUniqueConstraintViolation() {
        // Given
        userRepository.save(testUser);

        User duplicateUser = new User("differentuser", "[email protected]", "password");

        // When & Then
        assertThatThrownBy(() -> userRepository.saveAndFlush(duplicateUser))
            .hasCauseInstanceOf(org.hibernate.exception.ConstraintViolationException.class);
    }
}