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

💻 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.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);
    }
}