Tag Archives: Java

JUnit 6 with Mockito: Mocking, Spying, and Best Practices

Mockito and JUnit 6 are the most powerful duo in the Java testing toolkit. Mockito handles mock creation, stubbing, and verification while JUnit 6 orchestrates the test lifecycle. Together they let you test any unit in complete isolation, regardless of how many dependencies it has. This guide covers every Mockito feature you need β€” from basic mocking to advanced argument captors, spies, and common pitfalls to avoid.

Setup: Mockito with JUnit 6

<!-- Mockito Core + JUnit 5/6 integration -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.12.0</version>
    <scope>test</scope>
</dependency>
<!-- Note: spring-boot-starter-test already includes mockito-junit-jupiter -->

Activate Mockito’s annotation processing by adding @ExtendWith(MockitoExtension.class) to your test class:

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class) // enables @Mock, @InjectMocks, @Captor, @Spy
class PaymentServiceTest { }

Creating Mocks: @Mock vs Mockito.mock()

@ExtendWith(MockitoExtension.class)
class MockCreationTest {

    // Annotation style β€” cleanest, preferred for fields
    @Mock
    private PaymentGateway paymentGateway;

    @Mock
    private NotificationService notificationService;

    // Programmatic style β€” useful when you need a mock inside a method
    @Test
    void programmaticMockCreation() {
        // Create a mock inside the test method
        UserRepository mockRepo = Mockito.mock(UserRepository.class);
        when(mockRepo.findById(1L)).thenReturn(Optional.of(new User(1L, "Alice")));

        User found = mockRepo.findById(1L).orElseThrow();
        assertEquals("Alice", found.getName());
    }
}

Stubbing with when().thenReturn()

@ExtendWith(MockitoExtension.class)
class StubbingExamplesTest {

    @Mock private PaymentGateway paymentGateway;
    @InjectMocks private PaymentService paymentService;

    @Test
    @DisplayName("Successful payment returns a confirmation number")
    void successfulPaymentReturnsConfirmationNumber() {
        // Stub: when gateway.charge() is called with ANY double, return a confirmation
        when(paymentGateway.charge(anyDouble()))
            .thenReturn(new PaymentResult("CONF-12345", true));

        PaymentResult result = paymentService.processPayment(99.99);

        assertTrue(result.isSuccessful());
        assertEquals("CONF-12345", result.getConfirmationNumber());
    }

    @Test
    @DisplayName("Gateway failure propagates as PaymentException")
    void gatewayFailureThrowsPaymentException() {
        // Stub: throw an exception when the gateway is called
        when(paymentGateway.charge(anyDouble()))
            .thenThrow(new GatewayTimeoutException("Gateway timed out"));

        assertThrows(PaymentException.class,
            () -> paymentService.processPayment(99.99));
    }

    @Test
    @DisplayName("Multiple calls can return different values")
    void consecutiveCallsReturnDifferentValues() {
        // First call returns PENDING, second returns COMPLETED
        when(paymentGateway.checkStatus(anyString()))
            .thenReturn(PaymentStatus.PENDING)
            .thenReturn(PaymentStatus.COMPLETED);

        assertEquals(PaymentStatus.PENDING,   paymentGateway.checkStatus("TX-001"));
        assertEquals(PaymentStatus.COMPLETED, paymentGateway.checkStatus("TX-001"));
    }
}
Continue reading JUnit 6 with Mockito: Mocking, Spying, and Best Practices

Testing REST APIs with JUnit 6: MockMvc vs WebTestClient

Testing REST APIs is one of the most common tasks in any Spring Boot project. JUnit 6 supports two primary approaches: MockMvc for servlet-based (synchronous) APIs and WebTestClient for reactive (WebFlux) APIs β€” though WebTestClient can also test traditional Spring MVC applications. This guide covers both tools in depth with real request/response examples, JSON path assertions, and patterns for testing authentication, error responses, and pagination.

MockMvc vs WebTestClient: When to Use Which

MockMvcWebTestClient
TransportNo real HTTP β€” tests the servlet layer directlyReal or mock HTTP
Best forSpring MVC (traditional Servlet)WebFlux (reactive) or both
SpeedFastest β€” no networking overheadSlightly slower but more realistic
Fluent APIBuilder-style with matchersReactive, fluent chain
Works in @WebMvcTestβœ… Yes (auto-configured)βœ… Yes (with @AutoConfigureMockMvc)

Part 1: Testing REST APIs with MockMvc

Setup and Auto-Configuration

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.beans.factory.annotation.Autowired;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;

@SpringBootTest
@AutoConfigureMockMvc // injects MockMvc with full security and filters
@DisplayName("Product API β€” MockMvc tests")
class ProductApiMockMvcTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ProductRepository productRepository; // used for test data setup

    @BeforeEach
    void seedTestData() {
        productRepository.deleteAll();
        productRepository.saveAll(List.of(
            new Product(null, "Laptop",     999.00, "Electronics"),
            new Product(null, "Headphones",  79.00, "Electronics"),
            new Product(null, "Notebook",    5.99,  "Stationery")
        ));
    }

    @Test
    @DisplayName("GET /api/products returns 200 with all products")
    void getAllProductsReturns200() throws Exception {
        mockMvc.perform(get("/api/products")
                .accept("application/json"))
            .andDo(print()) // prints request + response to console for debugging
            .andExpect(status().isOk())
            .andExpect(content().contentType("application/json"))
            .andExpect(jsonPath("$").isArray())
            .andExpect(jsonPath("$.length()").value(3))
            .andExpect(jsonPath("$[0].name").value("Laptop"));
    }
}
Continue reading Testing REST APIs with JUnit 6: MockMvc vs WebTestClient

JUnit 6 with Spring Boot: Unit, Slice, and Integration Testing

Spring Boot and JUnit 6 are the most widely used combination in the Java testing ecosystem. Together they give you a layered testing strategy β€” fast pure unit tests, focused slice tests that load only part of the Spring context, and full integration tests with a running application. This guide covers all three layers with complete, production-ready examples.

The Three Testing Layers

LayerAnnotationSpeedSpring ContextUse for
Unit@Test onlyVery fast (<10ms)NoneService logic, utilities, domain objects
Slice@WebMvcTest, @DataJpaTest, etc.Fast (1–5s)Partial β€” one layer onlyControllers, repositories, JSON serialization
Integration@SpringBootTestSlow (5–30s)Full application contextEnd-to-end flows, real DB, real HTTP

Setup: Spring Boot Test Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <!-- spring-boot-starter-test bundles:
         junit-jupiter, mockito-core, assertj-core,
         spring-test, hamcrest, jsonpath -->
</dependency>

Layer 1: Pure Unit Tests (No Spring Context)

For service and domain logic, avoid loading a Spring context entirely. Instantiate the class directly and mock dependencies with Mockito:

import org.junit.jupiter.api.*;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

// @ExtendWith(MockitoExtension.class): activates Mockito annotation processing
// No Spring context loaded β€” this test runs in milliseconds
@ExtendWith(MockitoExtension.class)
@DisplayName("OrderService β€” unit tests")
class OrderServiceTest {

    // @Mock creates a Mockito mock and injects it into @InjectMocks
    @Mock
    private OrderRepository orderRepository;

    @Mock
    private EmailService emailService;

    // @InjectMocks creates an instance with mocked dependencies injected
    @InjectMocks
    private OrderService orderService;

    @Test
    @DisplayName("Creating an order saves it and sends a confirmation email")
    void creatingOrderSavesItAndSendsConfirmationEmail() {
        // Arrange: define mock behaviour
        Order savedOrder = new Order(1L, "[email protected]", 99.99, OrderStatus.PENDING);
        when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);

        // Act
        Order result = orderService.createOrder("[email protected]", 99.99);

        // Assert: verify result and interactions
        assertNotNull(result);
        assertEquals(OrderStatus.PENDING, result.getStatus());
        verify(orderRepository, times(1)).save(any(Order.class));
        verify(emailService,    times(1)).sendConfirmation("[email protected]");
    }

    @Test
    @DisplayName("Creating an order with negative total throws IllegalArgumentException")
    void negativeOrderTotalThrowsException() {
        assertThrows(IllegalArgumentException.class,
            () -> orderService.createOrder("[email protected]", -1.00));
        // Verify no repository or email interaction occurred
        verifyNoInteractions(orderRepository, emailService);
    }
}
Continue reading JUnit 6 with Spring Boot: Unit, Slice, and Integration Testing

Advanced Extensions in JUnit 6: Creating Custom Testing Frameworks

The JUnit 6 extension model is not just for simple before/after hooks. At its most advanced, it lets you build entire custom testing frameworks on top of JUnit β€” with domain-specific annotations, automatic injection, retry logic, soft assertions, and test templates. This guide explores the most powerful extension patterns used by production-grade testing frameworks, with complete, runnable examples.

This post builds on JUnit 6 Extensions Model: Build Custom Extensions Step-by-Step. Make sure you are comfortable with basic extension interfaces before diving in here.

Pattern 1: Composed Annotation Extensions

Combine multiple annotations and extensions into a single meta-annotation so test classes need only one annotation to get a full suite of capabilities:

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.*;
import java.lang.annotation.*;

/**
 * @DatabaseTest: a single annotation that applies:
 *  - @ExtendWith(DatabaseSetupExtension.class) β€” manages DB lifecycle
 *  - @ExtendWith(TransactionRollbackExtension.class) β€” rolls back after each test
 *  - @Tag("integration") β€” marks for integration test filtering
 *  - @Tag("database") β€” marks for database test filtering
 *  - @TestInstance(PER_CLASS) β€” shares one instance across methods
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(DatabaseSetupExtension.class)
@ExtendWith(TransactionRollbackExtension.class)
@Tag("integration")
@Tag("database")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public @interface DatabaseTest {
    // Custom attribute: which database profile to use
    String profile() default "test";
}

// DatabaseSetupExtension: opens connection before all, closes after all
class DatabaseSetupExtension implements BeforeAllCallback, AfterAllCallback {
    @Override
    public void beforeAll(ExtensionContext ctx) {
        System.out.println("[DB] Opening database connection");
        // Store connection in context store for tests to access
        ctx.getStore(ExtensionContext.Namespace.GLOBAL)
           .put("db.connection", createConnection());
    }
    @Override
    public void afterAll(ExtensionContext ctx) {
        System.out.println("[DB] Closing database connection");
    }
    private Object createConnection() { return "connection-placeholder"; }
}

// TransactionRollbackExtension: wraps each test in a transaction that rolls back
class TransactionRollbackExtension implements BeforeEachCallback, AfterEachCallback {
    @Override
    public void beforeEach(ExtensionContext ctx) {
        System.out.println("[TX] Starting transaction");
    }
    @Override
    public void afterEach(ExtensionContext ctx) {
        System.out.println("[TX] Rolling back transaction"); // ensures test isolation
    }
}

// Usage β€” one annotation gives you DB lifecycle + transaction rollback + tags
@DatabaseTest(profile = "integration")
class UserRepositoryTest {
    @Test
    void findByEmailReturnsCorrectUser() {
        // DB is set up, wrapped in a transaction that will roll back
        assertNotNull("result");
    }
}
Continue reading Advanced Extensions in JUnit 6: Creating Custom Testing Frameworks

Build Your Own JUnit 6 Test Engine (Advanced Guide)

Building your own JUnit 6 TestEngine is the ultimate act of framework mastery. It lets you run tests written in any format β€” YAML, XML, a custom DSL, plain text specifications β€” on the same JUnit Platform that runs your regular @Test methods. This guide builds a complete, working custom engine from scratch, step by step.

Before reading this, make sure you understand how the engine works internally β€” see JUnit 6 Internals: How the Test Engine Works first.

What We Will Build

We will build a CSV Test Engine that discovers .csv test files on the classpath, treats each row as a test case (input values + expected output), and runs them through a target method. This demonstrates every part of the TestEngine contract in a realistic, runnable example.

Step 1: Add the Platform Engine Dependency

<!-- JUnit Platform Engine SPI β€” provides TestEngine, TestDescriptor, etc. -->
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-engine</artifactId>
    <version>1.11.0</version>
    <scope>test</scope>
</dependency>

<!-- Commons CSV for parsing CSV test files -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-csv</artifactId>
    <version>1.10.0</version>
    <scope>test</scope>
</dependency>
Continue reading Build Your Own JUnit 6 Test Engine (Advanced Guide)

JUnit 6 Internals: How the Test Engine Works

Most developers treat JUnit 6 as a black box: annotate a method with @Test, run the build, see green or red. But understanding how the JUnit 6 test engine actually works β€” the internal phases, the test descriptor tree, the execution listener chain, the extension invocation order β€” makes you dramatically better at debugging mysterious failures, building custom tooling, and reasoning about framework behaviour.

This post walks through the complete internal lifecycle of a test run, from Launcher creation to final result reporting, with diagrams and code you can run yourself.

The Two Phases: Discovery and Execution

Every JUnit 6 test run has exactly two phases:

  1. Discovery Phase β€” The engine scans the classpath, resolves selectors and filters, and builds a TestDescriptor tree representing all tests that could run.
  2. Execution Phase β€” The engine walks the descriptor tree, fires lifecycle callbacks and extension hooks, executes test methods, and reports results through EngineExecutionListener.

These phases are intentionally separate. You can discover tests without executing them β€” this is how IDEs show the test tree before you click Run.

The TestDescriptor Tree

During discovery, JUnit builds a tree of TestDescriptor nodes. Each node represents a test container (engine, class, nested class) or a test (individual method). Here is what the tree looks like for a simple test class:

TestDescriptor Tree
└─ EngineDescriptor [junit-jupiter]
    └─ ClassTestDescriptor [CalculatorTest]
        β”œβ”€ TestMethodDescriptor [additionTest()]
        β”œβ”€ TestMethodDescriptor [subtractionTest()]
        └─ NestedClassTestDescriptor [WhenDividingByZero]
            └─ TestMethodDescriptor [throwsArithmeticException()]

Inspecting the Test Plan Programmatically

import org.junit.platform.launcher.*;
import org.junit.platform.launcher.core.*;
import org.junit.platform.engine.discovery.DiscoverySelectors;

public class TestPlanInspector {

    public static void main(String[] args) {
        // Build a discovery request for a specific package
        LauncherDiscoveryRequest discoveryRequest =
            LauncherDiscoveryRequestBuilder.request()
                .selectors(DiscoverySelectors.selectPackage("com.example"))
                .build();

        Launcher launcher = LauncherFactory.create();

        // DISCOVER: build the TestDescriptor tree without executing tests
        TestPlan testPlan = launcher.discover(discoveryRequest);

        // Walk the tree and print every node
        testPlan.getRoots().forEach(engine -> {
            System.out.println("Engine: " + engine.getDisplayName());
            printDescendants(testPlan, engine, 1);
        });
    }

    private static void printDescendants(TestPlan plan,
                                         TestIdentifier node,
                                         int depth) {
        String indent = "  ".repeat(depth);
        plan.getChildren(node).forEach(child -> {
            // isTest() = leaf node (a @Test method)
            // isContainer() = intermediate node (class, nested class, engine)
            String type = child.isTest() ? "[TEST]     " : "[CONTAINER]";
            System.out.printf("%s%s %s%n", indent, type, child.getDisplayName());
            printDescendants(plan, child, depth + 1);
        });
    }
}

Sample Output

Engine: JUnit Jupiter
  [CONTAINER] CalculatorTest
    [TEST]      additionTest()
    [TEST]      subtractionTest()
    [CONTAINER] WhenDividingByZero
      [TEST]    throwsArithmeticException()
Continue reading JUnit 6 Internals: How the Test Engine Works

Parallel Test Execution in JUnit 6: Configuration and Pitfalls

Running tests sequentially is safe but slow. A test suite that takes 10 minutes to run sequentially can often complete in 2–3 minutes with parallel execution. JUnit 6 has first-class support for parallel test execution β€” but it requires careful configuration to avoid flaky tests caused by shared state. This guide covers every configuration option, pitfall, and real-world pattern you need to enable parallel execution confidently.

How JUnit 6 Parallel Execution Works

JUnit 6 parallel execution is disabled by default. When enabled, JUnit can run tests at two levels:

  • Class-level parallelism β€” multiple test classes run concurrently in different threads
  • Method-level parallelism β€” multiple test methods within the same class run concurrently

These are configured independently, giving you fine-grained control over the degree of parallelism.

Step 1: Enable Parallel Execution

Create or edit src/test/resources/junit-platform.properties:

# Enable parallel execution (disabled by default)
junit.jupiter.execution.parallel.enabled=true

# Default execution mode for all tests
# 'concurrent' = run in parallel with other tests at the same level
# 'same_thread' = run in the same thread as parent (sequential)
junit.jupiter.execution.parallel.mode.default=concurrent

# Default execution mode for top-level test classes
# concurrent = classes run in parallel
# same_thread = classes run sequentially (but methods within might be concurrent)
junit.jupiter.execution.parallel.mode.classes.default=concurrent

Step 2: Configure the Thread Pool Strategy

# Strategy: 'dynamic' uses (availableProcessors * factor) threads
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0

# Strategy: 'fixed' uses an exact thread count
# junit.jupiter.execution.parallel.config.strategy=fixed
# junit.jupiter.execution.parallel.config.fixed.parallelism=4

# Strategy: 'custom' uses your own ParallelExecutionConfigurationStrategy
# junit.jupiter.execution.parallel.config.strategy=custom
# junit.jupiter.execution.parallel.config.custom.class=com.example.MyStrategy
Continue reading Parallel Test Execution in JUnit 6: Configuration and Pitfalls