00:00

Unit Testing with JUnit 5 & Mockito

Last updated:

Unit testing with JUnit 5 and Mockito means testing one class, such as a Spring Boot service, on its own, with its collaborators replaced by mocks. A mock is a stand-in object whose answers you script in the test, so you can check how your class reacts when the payment is approved, the stock runs out or a product doesn't exist. These tests need no database, no network and no Spring context, so hundreds of them run in seconds and point straight at the line that broke.

This tutorial explains where unit tests fit next to Spring Boot's other test types and how JUnit and Mockito work together. It covers the JUnit essentials (assertions, lifecycle, @Nested, parameterized tests) and the Mockito essentials: @Mock vs @InjectMocks, stubbing, verifying, argument captors and matchers. It then unit-tests a complete order service on Spring Boot 4.1 and explains the real Mockito errors you are most likely to meet.

1. What Is Unit Testing in Spring Boot?

A unit test checks a single class through its public methods. Everything the class depends on, such as repositories, HTTP clients and payment gateways, is replaced by a test double, so the test controls every input and sees every output. The class is created with new, not by Spring.

Two libraries do the work. JUnit finds and runs the test methods and reports the results. Mockito creates the mocks, lets you script their answers (stubbing) and checks how they were called (verification). Classes written with constructor injection are easy to unit-test, because the test can pass mocks straight into the constructor.

2. Unit Tests vs Slice Tests vs @SpringBootTest

Unit testSlice test (@WebMvcTest, @DataJpaTest)@SpringBootTest
Spring contextNonePart of itAll of it
CollaboratorsMockito mocks (@Mock)Real beans in the slice, mocks (@MockitoBean) for the restReal beans, some replaced with @MockitoBean
Speed per testMillisecondsContext start plus millisecondsFull context start plus milliseconds
FindsLogic and branching bugsMapping, JSON, SQL and validation bugsWiring and configuration bugs

Most of your tests should be unit tests, backed by a smaller number of slice and integration tests. The Spring Boot testing overview covers all three, and the Testcontainers tutorial covers testing against real databases.

3. How JUnit and Mockito Work Together

A unit test with Mockito mocksOrderServiceTest runs with MockitoExtension. It creates the real OrderService with new and calls placeOrder. OrderService calls four Mockito mocks: ProductCatalog, whose findPrice returns a scripted price; InventoryClient, whose reserve returns true or false; PaymentGateway, whose charge returns approved or declined; and OrderRepository, whose save returns its argument. It also uses Clock.fixed, a real object rather than a mock. The test first stubs answers with given, then calls the method, then checks the result and the calls with then and should. OrderServiceTest @ExtendWith(MockitoExtension) given(...) then(...).should() OrderService the real class under test created with new placeOrder() @Mock ProductCatalog findPrice → scripted price @Mock InventoryClient reserve → true / false @Mock PaymentGateway charge → approved / declined @Mock OrderRepository save → returns its argument Clock.fixed(...) a real object, not a mock 1. stub answers 2. call 3. check the result and the calls
Figure 1: only the class under test is real; its collaborators are mocks whose answers the test scripts and whose calls it checks.
  1. JUnit creates a new instance of the test class for every test method, so tests can't leak field values into each other.
  2. @ExtendWith(MockitoExtension.class) plugs Mockito into that lifecycle. Before each test it creates a fresh mock for every @Mock field.
  3. Your @BeforeEach method builds the class under test with those mocks.
  4. The test stubs the answers it needs, calls the real method, then checks the result and the calls made on the mocks.
  5. After each test, Mockito's strict stubs mode, the default with the extension, fails the test if a stub was never used (section 8.2).

4. Setup

spring-boot-starter-test brings everything in one dependency, with versions managed by Spring Boot:

pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
LibraryVersion in Spring Boot 4.1.2Used for
JUnit Jupiter6.0.3Running tests, assertions, parameterized tests
Mockito (core and JUnit extension)5.23.0Mocks, stubbing, verification
AssertJ3.27.7Readable, fluent assertions
Hamcrest, JSONassert, JsonPath, XMLUnit, Awaitility, Spring TestManagedMatchers, JSON and XML checks, waiting for async results, Spring test support
Version note: Spring Boot 4 ships JUnit 6. It keeps the same Jupiter programming model and org.junit.jupiter packages as JUnit 5, so everything on this page applies to both. It also raises the minimum Java version to 17. For Spring tests, @MockBean was removed in Spring Boot 4. Use @MockitoBean from org.springframework.test.context.bean.override.mockito.

On JDK 21 and later, Mockito prints a warning that it is "self-attaching" a Java agent, a mechanism that future JDKs will block. Register Mockito as an agent in the build instead:

pom.xml (build section)
<build>
    <plugins>
        <!-- Exposes the path of each dependency jar as a Maven property -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-dependency-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>properties</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <!-- Loads Mockito as a Java agent instead of letting it attach itself at runtime -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <argLine>-javaagent:${org.mockito:mockito-core:jar}</argLine>
            </configuration>
        </plugin>
    </plugins>
</build>

5. JUnit 5 Essentials

5.1 Test Methods and Assertions

A test is a method annotated with @Test. It doesn't need to be public, and neither does the class. Use AssertJ's assertThat for checks: the messages are clear, and code completion shows every available check. For exceptions, assertThatThrownBy checks the type and the message in one go:

OrderServiceTest.java (excerpts)
assertThat(confirmation).isEqualTo(
        new OrderConfirmation("KB-100", 2, new BigDecimal("178.00"), "TX-9001"));

assertThatThrownBy(() -> orderService.placeOrder(new PlaceOrderRequest("C-42", "XX-999", 1)))
        .isInstanceOf(ProductNotFoundException.class)
        .hasMessage("Product not found: XX-999");

5.2 Lifecycle Annotations

AnnotationRunsTypical use
@BeforeEachBefore every test methodCreate the class under test with fresh mocks
@AfterEachAfter every test methodClean up files or static state
@BeforeAll / @AfterAllOnce per class (static methods)Expensive shared setup
@DisabledNeverTemporarily skip a test, with a reason

5.3 Readable Tests with @DisplayName and @Nested

@DisplayName gives a test a sentence-like name in reports. @Nested groups tests that share a situation into an inner class with its own @BeforeEach, which runs after the outer one. In the example, the HappyPath class stubs the price and stock once for all three of its tests.

5.4 Parameterized Tests

@ParameterizedTest runs one test method once per row of input. @ValueSource supplies single values, @CsvSource supplies rows, and JUnit converts each column to the parameter type, here including BigDecimal. Boundary values belong in these tables:

OrderTotalTest.java
package com.javabytechie.shop.order;

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

import java.math.BigDecimal;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class OrderTotalTest {

    @DisplayName("Order total with 10% off from 1000.00")
    @ParameterizedTest(name = "{1} x {0} = {2}")
    @CsvSource({
            "89.00,   1, 89.00",     // no discount
            "333.33,  3, 999.99",    // just below the threshold
            "500.00,  2, 900.00",    // exactly 1000.00: discount applies
            "120.00, 10, 1080.00"    // well above the threshold
    })
    void totalAppliesDiscountFromThreshold(BigDecimal unitPrice, int quantity, BigDecimal expected) {
        assertThat(OrderService.totalFor(unitPrice, quantity)).isEqualTo(expected);
    }
}

For complex objects, @MethodSource("name") reads the arguments from a static method that returns a Stream.

6. Mockito Essentials

6.1 @Mock vs @InjectMocks vs @Spy vs @MockitoBean

AnnotationCreatesUse it for
@MockA fake whose methods return defaults (null, 0, false, empty Optional or collection) until stubbedEvery collaborator of the class under test
@InjectMocksThe real class under test, with the @Mock fields passed to its constructorClasses whose dependencies are all mocks
@SpyA real object whose methods you can partly stub and verifyRarely; usually a sign the class does too much
@CaptorAn ArgumentCaptorInspecting the objects passed to a mock
@MockitoBean (Spring)A mock that replaces a bean in the Spring contextSlice tests and @SpringBootTest, not plain unit tests

The example builds OrderService by hand in @BeforeEach instead of using @InjectMocks. One of its dependencies is a real fixed Clock, not a mock, and @InjectMocks would silently pass null for it (section 8.5).

6.2 Stubbing: given() and when()

Stubbing tells a mock what to return. Mockito has two spellings that do the same thing. The BDD style from BDDMockito reads naturally in "given / when / then" tests, and this page uses it:

PurposeClassic (Mockito)BDD (BDDMockito)
Return a valuewhen(mock.call()).thenReturn(x)given(mock.call()).willReturn(x)
Throwwhen(mock.call()).thenThrow(ex)given(mock.call()).willThrow(ex)
Compute the answerthenAnswer(inv -> ...)willAnswer(inv -> ...)
Verify a callverify(mock).call()then(mock).should().call()

willAnswer(invocation -> invocation.getArgument(0)) makes a mocked save return whatever it was given, a common pattern for repositories.

6.3 Verifying Interactions

When a method's job is a side effect, such as charging a card or releasing stock, verify the call. Verify what must not happen just as carefully:

OrderServiceTest.java (excerpts)
// The payment was declined: stock must be released and nothing saved
then(inventory).should().release("KB-100", 2);
then(orders).should(never()).save(any());

// Out of stock: the customer must never be charged
verifyNoInteractions(payments, orders);

// Order matters: reserve, then charge, then save
InOrder steps = inOrder(inventory, payments, orders);
steps.verify(inventory).reserve("KB-100", 2);
steps.verify(payments).charge("C-42", new BigDecimal("178.00"));
steps.verify(orders).save(any(Order.class));

Don't verify every stubbed call. If a stubbed value is used in the result, checking the result already proves the call happened.

6.4 Capturing Arguments with ArgumentCaptor

When the class under test builds an object and passes it on, an ArgumentCaptor grabs that object so you can check its fields:

OrderServiceTest.java (excerpt)
@Captor
ArgumentCaptor<Order> savedOrder;

// ...
then(orders).should().save(savedOrder.capture());
assertThat(savedOrder.getValue().placedAt()).isEqualTo(NOW);
assertThat(savedOrder.getValue().transactionId()).isEqualTo("TX-9001");

6.5 Argument Matchers

Matchers such as any(), anyString(), eq(...) and argThat(...) match arguments loosely. Use exact values when they matter to the test, and matchers when they don't. In one call, either all arguments are matchers or none are (section 8.3). Plain values are compared with equals, which matters for BigDecimal: new BigDecimal("178") does not equal new BigDecimal("178.00") (section 8.4).

7. A Complete Example: Unit Testing an Order Service

OrderService.placeOrder validates the quantity, looks up the price, applies a 10% discount from 1000.00, reserves stock, charges the customer, and saves the order. If the payment is declined, it releases the stock again. Each step can fail, and each failure must stop the steps after it. The service only throws domain exceptions; turning them into HTTP responses belongs to the web layer, as shown in the exception handling tutorial:

Paths through OrderService.placeOrder and the outcome of eachplaceOrder runs five steps in order: check quantity, findPrice, reserve stock, charge and save order. An invalid quantity such as 0, -1 or 11 throws InvalidQuantityException and calls no collaborator. An empty price throws ProductNotFoundException with no stock, payment or save. If reserve returns false, OutOfStockException is thrown with no charge and no save. If the charge is declined, the stock is released, PaymentDeclinedException is thrown and nothing is saved. On the happy path the order is saved with the fixed time and an OrderConfirmation is returned. 1. check quantity quantity 0, -1, 11 InvalidQuantityException no collaborator called 2. findPrice price is empty ProductNotFoundException no stock, payment or save 3. reserve stock reserve returns false OutOfStockException no charge, no save 4. charge charge declined PaymentDeclinedException stock released, no save 5. save order happy path OrderConfirmation saved with fixed time
Figure 2: one test per path; failure-path tests also verify that the later steps never ran.

7.1 The Code Under Test

package com.javabytechie.shop.order;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Clock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    static final BigDecimal DISCOUNT_THRESHOLD = new BigDecimal("1000.00");
    static final BigDecimal DISCOUNT_RATE = new BigDecimal("0.10");

    private final ProductCatalog catalog;
    private final InventoryClient inventory;
    private final PaymentGateway payments;
    private final OrderRepository orders;
    private final Clock clock;

    public OrderService(ProductCatalog catalog, InventoryClient inventory, PaymentGateway payments,
                        OrderRepository orders, Clock clock) {
        this.catalog = catalog;
        this.inventory = inventory;
        this.payments = payments;
        this.orders = orders;
        this.clock = clock;
    }

    public OrderConfirmation placeOrder(PlaceOrderRequest request) {
        if (request.quantity() < 1 || request.quantity() > 10) {
            throw new InvalidQuantityException(request.quantity());
        }
        BigDecimal unitPrice = catalog.findPrice(request.sku())
                .orElseThrow(() -> new ProductNotFoundException(request.sku()));
        BigDecimal total = totalFor(unitPrice, request.quantity());

        if (!inventory.reserve(request.sku(), request.quantity())) {
            throw new OutOfStockException(request.sku(), request.quantity());
        }

        PaymentResult payment = payments.charge(request.customerId(), total);
        if (!payment.approved()) {
            // Give the stock back, otherwise it stays reserved for an order that never happened
            inventory.release(request.sku(), request.quantity());
            throw new PaymentDeclinedException(request.customerId());
        }

        Order saved = orders.save(new Order(request.customerId(), request.sku(), request.quantity(),
                total, payment.transactionId(), clock.instant()));
        log.info("Order placed: {} x {} for {} ({})", saved.quantity(), saved.sku(), saved.total(), saved.transactionId());
        return new OrderConfirmation(saved.sku(), saved.quantity(), saved.total(), saved.transactionId());
    }

    // Orders of 1000.00 or more get 10% off
    static BigDecimal totalFor(BigDecimal unitPrice, int quantity) {
        BigDecimal subtotal = unitPrice.multiply(BigDecimal.valueOf(quantity));
        if (subtotal.compareTo(DISCOUNT_THRESHOLD) >= 0) {
            subtotal = subtotal.subtract(subtotal.multiply(DISCOUNT_RATE));
        }
        return subtotal.setScale(2, RoundingMode.HALF_UP);
    }
}

7.2 The Unit Test

Every path in Figure 2 has a test. Each one checks both the result and what did, or didn't, happen to the collaborators:

OrderServiceTest.java
package com.javabytechie.shop.order;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verifyNoInteractions;

import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    private static final Instant NOW = Instant.parse("2026-10-25T09:30:00Z");

    @Mock
    ProductCatalog catalog;

    @Mock
    InventoryClient inventory;

    @Mock
    PaymentGateway payments;

    @Mock
    OrderRepository orders;

    @Captor
    ArgumentCaptor<Order> savedOrder;

    OrderService orderService;

    @BeforeEach
    void setUp() {
        // A fixed clock makes placedAt predictable; the mocks come from MockitoExtension
        Clock clock = Clock.fixed(NOW, ZoneOffset.UTC);
        orderService = new OrderService(catalog, inventory, payments, orders, clock);
    }

    @Nested
    @DisplayName("when the product exists and is in stock")
    class HappyPath {

        @BeforeEach
        void stockAndPrice() {
            given(catalog.findPrice("KB-100")).willReturn(Optional.of(new BigDecimal("89.00")));
            given(inventory.reserve("KB-100", 2)).willReturn(true);
        }

        @Test
        @DisplayName("charges the customer and saves the order")
        void placesOrder() {
            given(payments.charge("C-42", new BigDecimal("178.00"))).willReturn(new PaymentResult("TX-9001", true));
            given(orders.save(any(Order.class))).willAnswer(invocation -> invocation.getArgument(0));

            OrderConfirmation confirmation = orderService.placeOrder(new PlaceOrderRequest("C-42", "KB-100", 2));

            assertThat(confirmation).isEqualTo(new OrderConfirmation("KB-100", 2, new BigDecimal("178.00"), "TX-9001"));
            then(orders).should().save(savedOrder.capture());
            assertThat(savedOrder.getValue().placedAt()).isEqualTo(NOW);
            assertThat(savedOrder.getValue().transactionId()).isEqualTo("TX-9001");
        }

        @Test
        @DisplayName("reserves stock before charging, and charges before saving")
        void callsCollaboratorsInOrder() {
            given(payments.charge(anyString(), any())).willReturn(new PaymentResult("TX-9002", true));
            given(orders.save(any(Order.class))).willAnswer(invocation -> invocation.getArgument(0));

            orderService.placeOrder(new PlaceOrderRequest("C-42", "KB-100", 2));

            InOrder steps = inOrder(inventory, payments, orders);
            steps.verify(inventory).reserve("KB-100", 2);
            steps.verify(payments).charge("C-42", new BigDecimal("178.00"));
            steps.verify(orders).save(any(Order.class));
        }

        @Test
        @DisplayName("releases the stock and saves nothing when the payment is declined")
        void releasesStockWhenPaymentDeclined() {
            given(payments.charge(anyString(), any())).willReturn(new PaymentResult(null, false));

            assertThatThrownBy(() -> orderService.placeOrder(new PlaceOrderRequest("C-42", "KB-100", 2)))
                    .isInstanceOf(PaymentDeclinedException.class)
                    .hasMessage("Payment declined for customer C-42");

            then(inventory).should().release("KB-100", 2);
            then(orders).should(never()).save(any());
        }
    }

    @Test
    @DisplayName("charges the discounted total for large orders")
    void chargesDiscountedTotal() {
        given(catalog.findPrice("MN-500")).willReturn(Optional.of(new BigDecimal("120.00")));
        given(inventory.reserve("MN-500", 10)).willReturn(true);
        given(payments.charge(anyString(), any())).willReturn(new PaymentResult("TX-9003", true));
        given(orders.save(any(Order.class))).willAnswer(invocation -> invocation.getArgument(0));

        orderService.placeOrder(new PlaceOrderRequest("C-42", "MN-500", 10));

        then(payments).should().charge("C-42", new BigDecimal("1080.00"));
    }

    @Test
    @DisplayName("does not charge when the product is out of stock")
    void outOfStock() {
        given(catalog.findPrice("KB-100")).willReturn(Optional.of(new BigDecimal("89.00")));
        given(inventory.reserve("KB-100", 5)).willReturn(false);

        assertThatThrownBy(() -> orderService.placeOrder(new PlaceOrderRequest("C-42", "KB-100", 5)))
                .isInstanceOf(OutOfStockException.class);

        verifyNoInteractions(payments, orders);
    }

    @Test
    @DisplayName("fails fast for an unknown product")
    void unknownProduct() {
        given(catalog.findPrice("XX-999")).willReturn(Optional.empty());

        assertThatThrownBy(() -> orderService.placeOrder(new PlaceOrderRequest("C-42", "XX-999", 1)))
                .isInstanceOf(ProductNotFoundException.class)
                .hasMessage("Product not found: XX-999");

        verifyNoInteractions(inventory, payments, orders);
    }

    @ParameterizedTest(name = "quantity {0} is rejected")
    @ValueSource(ints = {0, -1, 11})
    void rejectsInvalidQuantity(int quantity) {
        assertThatThrownBy(() -> orderService.placeOrder(new PlaceOrderRequest("C-42", "KB-100", quantity)))
                .isInstanceOf(InvalidQuantityException.class);

        verifyNoInteractions(catalog, inventory, payments, orders);
        then(inventory).should(never()).reserve(anyString(), anyInt());
    }
}

A fixed Clock makes the saved timestamp predictable. That is why the service takes a Clock instead of calling Instant.now().

7.3 Run the Tests

Console (./mvnw test)
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 -- in com.javabytechie.shop.order.OrderTotalTest
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 -- in com.javabytechie.shop.order.OrderServiceTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -- in com.javabytechie.shop.order.OrderServiceSpringTest
[INFO] Tests run: 14, Failures: 0, Errors: 0, Skipped: 0

After the first test has loaded Mockito, each unit test took 1 to 5 milliseconds. For comparison, the same happy path as a @SpringBootTest with four @MockitoBean fields first had to start the application context, which took 0.22 seconds even for this tiny app. In real applications with a database and web layer, that start takes several seconds.

OrderServiceSpringTest.java
package com.javabytechie.shop.order;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;

import java.math.BigDecimal;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

// The same scenario with the Spring context, to compare startup cost with the plain unit test
@SpringBootTest
class OrderServiceSpringTest {

    @MockitoBean
    ProductCatalog catalog;

    @MockitoBean
    InventoryClient inventory;

    @MockitoBean
    PaymentGateway payments;

    @MockitoBean
    OrderRepository orders;

    @Autowired
    OrderService orderService;

    @Test
    void placesOrderThroughTheSpringBean() {
        given(catalog.findPrice("KB-100")).willReturn(Optional.of(new BigDecimal("89.00")));
        given(inventory.reserve("KB-100", 2)).willReturn(true);
        given(payments.charge(anyString(), any())).willReturn(new PaymentResult("TX-9001", true));
        given(orders.save(any(Order.class))).willAnswer(invocation -> invocation.getArgument(0));

        assertThat(orderService.placeOrder(new PlaceOrderRequest("C-42", "KB-100", 2)).total())
                .isEqualByComparingTo("178.00");
    }
}

8. Common Mockito Errors and How to Fix Them

8.1 NullPointerException: "this.catalog" Is Null

The @Mock fields were never filled, because nothing told JUnit to run Mockito:

Console
java.lang.NullPointerException: Cannot invoke "com.javabytechie.shop.order.ProductCatalog.findPrice(String)" because "this.catalog" is null

How to fix it: add @ExtendWith(MockitoExtension.class) to the test class.

8.2 UnnecessaryStubbingException

A test stubbed the price lookup, but the quantity was invalid, so the service never asked for the price:

Console
org.mockito.exceptions.misusing.UnnecessaryStubbingException: 
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
  1. -> at your test method (YourTest.java:line)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.

How to fix it: delete the unused stub. It hides what the test really depends on. If a stub in a shared @BeforeEach really is used by only some tests, move it into those tests or into a @Nested class. Mark a stub lenient() only as a last resort.

8.3 InvalidUseOfMatchersException

Console
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at your test method (YourTest.java:line)
This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");

The stub mixed a raw value with a matcher: given(payments.charge("C-42", any())). How to fix it: wrap raw values in eq(), as in charge(eq("C-42"), any()), or use plain values for all arguments.

8.4 PotentialStubbingProblem: Argument Mismatch

The test stubbed charge("C-42", new BigDecimal("178")), but the service passed 178.00. BigDecimal.equals compares the scale too, so the stub never matched, and strict stubs reported it straight away instead of returning null:

Console
org.mockito.exceptions.misusing.PotentialStubbingProblem: 
Strict stubbing argument mismatch. Please check:
 - this invocation of 'charge' method:
    payments.charge("C-42", 178.00);
    -> at com.javabytechie.shop.order.OrderService.placeOrder(OrderService.java:46)
 - has following stubbing(s) with different arguments:
    1. payments.charge("C-42", 178); stubbed with: [Returns: PaymentResult[transactionId=TX-1, approved=true]]
      -> at your test method (YourTest.java:line)

How to fix it: stub with exactly the value the code produces (new BigDecimal("178.00")), or match by value with argThat(amount -> amount.compareTo(new BigDecimal("178")) == 0).

8.5 NullPointerException with @InjectMocks

@InjectMocks passes a mock for each constructor parameter it can match, and null for the rest. With no Clock mock, OrderService received null:

Console
java.lang.NullPointerException: Cannot invoke "java.time.Clock.instant()" because "this.clock" is null

How to fix it: create the class yourself in @BeforeEach when some dependencies are real objects, as the example does. The constructor then shows every dependency, and the compiler checks them.

8.6 "Argument(s) Are Different!" on verify

A verification expected release("KB-100", 3), but the service released 2. Mockito lists the calls that did happen, which usually points straight at the mistake:

Console
Argument(s) are different! Wanted:
inventory.release("KB-100", 3);
-> at your test method (YourTest.java:line)
Actual invocations have different arguments at position [1]:
inventory.reserve("KB-100", 2);
-> at com.javabytechie.shop.order.OrderService.placeOrder(OrderService.java:42)
inventory.release("KB-100", 2);

8.7 "Mockito Is Currently Self-Attaching"

Console
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
WARNING: A Java agent has been loaded dynamically (.../byte-buddy-agent-1.18.15.jar)
WARNING: Dynamic loading of agents will be disallowed by default in a future release

This is only a warning today, but future JDKs will refuse to load agents at runtime. How to fix it: add the Surefire -javaagent configuration from section 4. With it, the warning disappears and all tests still pass.

9. When to Write Unit Tests (and When Not To)

Good fit for a unit testBetter tested another way
Business rules: discounts, limits, status changesSQL queries and mappings: @DataJpaTest or Testcontainers
Branching and error handling in servicesJSON, validation and HTTP status codes: @WebMvcTest
Calculations and boundary valuesSpring configuration and wiring: @SpringBootTest
Orchestration: which collaborator is called, and whenThin classes that only delegate, with no logic of their own

10. Best Practices

  • Test behaviour, not implementation: assert on results and important side effects, not on every internal call.
  • Use constructor injection in production code, so tests can create classes with new.
  • Mock only what you don't own or can't run fast: gateways, repositories, clients. Use real records and value objects.
  • Inject time as a Clock, and use Clock.fixed in tests.
  • Keep Mockito's strict stubs and remove unnecessary stubs instead of making them lenient.
  • Verify what must not happen with never() and verifyNoInteractions on failure paths.
  • Give each test one reason to fail, and name it with @DisplayName.
  • Cover boundaries with parameterized tests: just below, at and above every limit.
  • Prefer AssertJ for assertions, including assertThatThrownBy for exceptions.
  • Register Mockito as a Java agent in the build on JDK 21 and later.
  • Save @SpringBootTest for wiring; don't start a context to test plain logic.

11. Summary

Unit testing with JUnit 5 and Mockito tests one Spring Boot class at a time, with its collaborators replaced by mocks and no Spring context at all. Add spring-boot-starter-test, annotate the test class with @ExtendWith(MockitoExtension.class), declare collaborators as @Mock, and build the class under test in @BeforeEach. Stub answers with given(...), call the real method, then check the result with AssertJ and the side effects with then(...).should(), never(), InOrder and ArgumentCaptor. Cover boundaries with parameterized tests and group scenarios with @Nested. The main recommendation: test every path through your business logic with fast unit tests, and keep slower Spring tests for the wiring and infrastructure that unit tests can't reach.