Power Your Software Testing with AI and Cloud

Supercharge QA with AI for Faster & Smarter Software Testing

Next-Gen App & Browser Testing Cloud

How to Use Mockito Spy for Effective Unit Testing in Java

Learn how to use Mockito Spy for partial mocking in Java. Discover when to use it, how to stub methods, and verify interactions effectively in unit tests.

Published on: November 9, 2025

  • Share:

Mockito spy is a powerful feature that enables developers to write class-based tests that directly interact with real Java objects. A Mockito spy acts as a wrapper around a real instance, letting you track method calls, verify interactions, and selectively stub specific methods when needed.

This makes Mockito spy very useful when you want to test how methods are invoked while retaining the real implementation. While many developers are familiar with Mockito mocks, which replace entire objects with simulated versions, Mockito spies provide finer control by combining real behavior with verification.

Overview

What Is a Mockito Spy?

Mockito’s spying capability bridges the gap between real object behavior and controlled testing scenarios. Once you understand how spies differ from mocks, it’s easier to appreciate how similar concepts are handled in other ecosystems.

When Should a Spy Be Used In Mockito?

You should use a spy in Mockito when your testing goal involves partial verification of a real object’s logic rather than full simulation.

  • Verify Interactions With Real Objects While Preserving Their Logic: Use a spy to ensure real methods execute while confirming correct interactions and parameters.
  • Stub Specific Methods While Keeping Others Real: Use a spy to replace external or unstable calls while maintaining realistic behavior for remaining methods.
  • Handle Complex Dependencies or Avoid Side Effects: Use a spy to isolate external dependencies, preventing side effects and ensuring faster, more predictable tests.
  • Combine Real Behavior With Test Flexibility: Use a spy to dynamically control responses, simulate edge cases, and fine-tune real behavior during testing.

How to Create and Use Mockito Spy?

You can create them in multiple ways, depending on how much flexibility or structure your test setup requires.

  • Create Manually With Mockito.spy(): Provides full control within the test body, ideal for flexible setups.
  • Use @Spy Annotation: Offers a cleaner, annotation-driven approach suitable for JUnit test classes.
  • Override Specific Mthods: Apply doReturn() or doAnswer() to alter behavior without affecting real logic.
  • Simulate Hybrid Workflows: Combine real execution with selective stubbing for layered services or API interactions.

Mockito Spy vs Mockito Mock, Which to Choose?

Mockito Spy and Mockito Mock serve different purposes in testing. Your choice depends on whether you need real behavior or complete isolation during test execution.

  • Choose Mockito Spy When: You need realism and want to observe how your code interacts with genuine business logic. It’s the better choice for integration-heavy or stateful scenarios where you still want verification power.
  • Choose Mockito Mock When: Your focus is pure isolation, testing your class without any dependency on real implementations. Mocks provide a clean slate, faster execution, and total control, making them ideal for unit tests with minimal side effects.

What Is a Mockito Spy?

A Mockito Spy enables partial mocking, meaning some methods can retain their original behavior while others can be overridden or verified during testing, allowing both real method calls and controlled customization of specific methods.

Mockito is a Java-based unit testing framework widely used with JUnit for Java unit testing It helps developers isolate behavior, create mock dependencies, and validate interactions effectively during automated testing.

When Should a Spy Be Used In Mockito?

A Spy in Mockito is primarily used during unit testing when you want to partially mock a real object, allowing actual methods to execute while still controlling the behavior of specific ones. This approach helps ensure that your tests reflect real application logic while maintaining flexibility and isolation.

Mockito plays an important role in improving the test independence of components by allowing you to create objects without relying on complex configurations or external dependencies.

It’s most commonly used with the JUnit framework to write cleaner and more maintainable tests. You can explore how Mockito integrates with JUnit in this detailed guide: JUnit 5 Mockito Tutorial.

Here are the key advantages of using Playwright Page Object Model:

  • Verify Interactions With Real Objects While Preserving Their Logic: Use a spy when you want to observe how real methods are called without losing their original functionality.
  • Stub Specific Methods While Keeping Others Real: Spies are ideal when only certain methods need controlled behavior, while the rest should execute as in the real object.
  • Handle Complex Dependencies or Avoid Side Effects: When a class has dependencies that perform actions like database operations or network calls, spies let you skip those methods while still testing the rest.
  • Combine Real Behavior with Test Flexibility: A spy allows you to partially mock behavior, giving you a balance between realism and control in your unit tests.
Note

Note: Run automation tests at scale across 3000+ browsers and OS combinations. Try LambdaTest Now!

How to Create and Use Mockito Spy?

Mockito spy provides a practical way to monitor and control real objects during JUnit testing. Instead of creating a pure mock, you can use a spy to observe how the actual object behaves, verify method calls, and selectively modify outcomes when needed.

This approach keeps the object’s real logic intact while still offering the flexibility to stub or track specific methods for testing purposes.

Before diving into tests, you can clone the Mockito-Spy GitHub repository, which contains examples of spying on real-world API classes, methods, and calls.

In Mockito, there are two common approaches to creating a Mockito spy:

  • Using the Mockito.spy() method.
  • Using the @Spy annotation.

Both approaches serve almost the same purpose but differ in setup and convenience.

The Mockito.spy() is ideal for quick, inline tests where you manually create and control the spy instance within your test code.

While the @Spy annotations work best for larger test setups, where you can simply annotate a field and let Mockito handle the initialization automatically (for example, using @ExtendWith (MockitoExtension.class)), resulting in cleaner and more maintainable code.

mockito spy new object

In the following subsections, you will learn how to create and use both the approaches of Mockito spy.

Using Mockito.spy() Method

The Mockito.spy() method is used to manually create a spy instance in your tests. It takes a real object as an argument and wraps it with a spy, allowing you to call real methods while still having the option to stub or verify interactions.

You can write it directly as Mockito.spy() in your test code. For example, in the test file CompaniesSpyTest.java, you can create a spy on the CompaniesService class to find the company with the fewest employees:

package com.alexanie.app.company;

import com.alexanie.app.model.Companies;
import com.alexanie.app.service.CompaniesService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.Arrays;
import java.util.List;

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

public class CompaniesSpyTest {

    @Test
    @DisplayName("Spy on CompaniesService and find company with fewest employees")
    void spyCompaniesService_findMinEmployees() {
        // Create a spy of the real service
        CompaniesService realService = Mockito.spy(new CompaniesService(null));
        // Stub the getAllCompanies() method with fake data
        List<Companies> fakeCompanies = Arrays.asList(
                new Companies(1L, "TechNova", 1200, Arrays.asList("AI", "Cloud")),
                new Companies(2L, "CodeSmiths", 500, Arrays.asList("Software Development", "DevOps")),
                new Companies(3L, "CyberSecPro", 300, Arrays.asList("Cybersecurity", "Networking"))
        );

        Mockito.doReturn(fakeCompanies).when(realService).getAllCompanies();

        // Use the spy to call the method
        List<Companies> companies = realService.getAllCompanies();

        // Find the company with the fewest employees
        Companies minCompany = companies.stream()
                .min((c1, c2) -> Integer.compare(c1.getNumberOfEmployees(), c2.getNumberOfEmployees()))
                .orElseThrow();

        // Assertions
        assertThat(minCompany.getName()).isEqualTo("CyberSecPro");
        assertThat(minCompany.getNumberOfEmployees()).isEqualTo(300);

        // Verify method interaction
        Mockito.verify(realService).getAllCompanies();
    }
}

LambdaTest

Code Walkthrough:

  • Creating a Spy Instance: You create a spy using Mockito.spy(new CompaniesService(null)) and assign it to realService.
  • Stubbing a Method: You stub the getAllCompanies() method to return a predefined list of fake companies.
  • Finding the Smallest Company: You use a comparator to find the company with the fewest employees.
  • Validating the Result: With assertThat, you check that the returned company matches the expected one, CyberSecPro, with 300 employees.
  • Verifying Method Calls: Finally, you verify that getAllCompanies() was called on the spy instance.

Using @Spy Annotation

The @Spy annotation provides an alternative way to create spies in Mockito. It serves the same purpose as the Mockito.spy() method but offers a cleaner and more declarative setup for automation testing and unit testing frameworks like JUnit, which is commonly used to run and manage Mockito-based tests.

mockito spy annotation

To use @Spy, simply annotate the field you want to spy on. Mockito will automatically initialize the spy when the test class is run, provided that the class is annotated with @ExtendWith(MockitoExtension.class). This extension ensures that Mockito processes annotations such as @Spy, @Mock, and @InjectMocks.

For example, in the CompaniesSpyAnnotationTest.java file under the company package:

package com.alexanie.app.company;

import com.alexanie.app.model.Companies;
import com.alexanie.app.service.CompaniesService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
public class CompaniesSpyAnnotationTest {

    @Spy
    private CompaniesService companiesService = new CompaniesService(null);    // real object, repo null for demo

    @Test
    @DisplayName("Find company by industry using @Spy")
    void spyCompaniesService_findByIndustry() {
        // Fake companies dataset
        List<Companies> fakeCompanies = Arrays.asList(
                new Companies(1L, "TechNova", 1200, Arrays.asList("AI", "Cloud")),
                new Companies(2L, "CodeSmiths", 500, Arrays.asList("Software Development", "DevOps")),
                new Companies(3L, "CyberSecPro", 300, Arrays.asList("Cybersecurity", "Networking"))
        );

        // Stub the getAllCompanies() method on the spy
        doReturn(fakeCompanies).when(companiesService).getAllCompanies();

        // Find company that belongs to the "AI" industry
        Companies aiCompany = companiesService.getAllCompanies().stream()
                .filter(c -> c.getIndustries().contains("AI"))
                .findFirst()
                .orElseThrow();

        // Assertions
        assertThat(aiCompany.getName()).isEqualTo("TechNova");
        assertThat(aiCompany.getIndustries()).contains("AI");

        // Verify interaction with the spy
        verify(companiesService).getAllCompanies();
    }
}

Code Walkthrough:

  • Declaring a Spy: You declare companiesService as a spy using the @Spy annotation.
  • Stubbing a Method: With doReturn(...).when(...), you stub the getAllCompanies() method to return a predefined list of fake companies.
  • Filtering Data: You filter the list to find the company that operates in the AI industry.
  • Validating the Result: Using assertThat, you confirm that the result matches the expected company (TechNova).
  • Verifying Method Calls: Finally, you use verify to ensure that the spy’s getAllCompanies() method was invoked.

Using @InjectMocks with @Spy

The @InjectMocks annotation in Mockito is used to automatically inject mock or spy dependencies into the class under test. When combined with @Spy, you can test a real object while still controlling certain methods with stubbing.

This is very useful when you want to partially stub some behaviors but still execute the actual implementation of other methods.

mockito spy injectmock

In this example, you’ll create a spy on the EmployeeService and use @InjectMocks to inject it into your test. You’ll then determine which profession earns the highest salary from a list of employees. Create a new file in your employee package under the test folder as EmployeeSpyInjectMocksTest.java and then type the code below.

package com.alexanie.app.employee;

import com.alexanie.app.model.Employee;
import com.alexanie.app.service.EmployeeService;
import com.alexanie.app.controller.EmployeeController;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
class EmployeeSpyInjectMocksTest {

   @Spy
   private EmployeeService employeeService = new EmployeeService(null); // spy real service

   @InjectMocks
   private EmployeeController employeeController; // controller with service injected

   @Test
   @DisplayName("Find profession with highest salary using @Spy + @InjectMocks")
   void findTopEarningProfession() {
       // Fake dataset
       List<Employee> fakeEmployees = Arrays.asList(
               new Employee(1L, "Alice Johnson", "Backend Developer", 95000.0,
                       Arrays.asList("Java", "Spring Boot", "SQL")),
               new Employee(2L, "Bob Smith", "Frontend Developer", 87000.0,
                       Arrays.asList("JavaScript", "React", "CSS")),
               new Employee(3L, "David Kim", "Data Scientist", 115000.0,
                       Arrays.asList("Python", "TensorFlow", "Pandas"))
       );

       // Stub the spy service directly
       doReturn(fakeEmployees).when(employeeService).getAllEmployees();

       // Use controller method
       List<Employee> employees = employeeController.getAllEmployees();

       // Find the highest earner
       Employee topEarner = employees.stream()
               .max(Comparator.comparingDouble(Employee::getSalary))
               .orElseThrow();

       // Assertions
       assertThat(topEarner.getProfession()).isEqualTo("Data Scientist");
       assertThat(topEarner.getSalary()).isEqualTo(115000.0);

       // Verify service interaction
       verify(employeeService).getAllEmployees();
   }
}

Annotations are a common technique used in Mockito and other frameworks, such as unit testing with Jest, Mocha, or Jasmine.

Code Walkthrough:

  • Annotating the Service: You annotate employeeService with @Spy, which wraps a real instance of the class.
  • Injecting the Spy: With @InjectMocks, Mockito automatically injects the spy into the test subject (employeeController).
  • Stubbing a Method: You stub the getAllEmployees() method to return a fake dataset.
  • Processing the Data: You use the controller to determine which employee has the highest salary.
  • Validating the Result: Using assertThat, you confirm that the top earner is David Kim, the Data Scientist.
  • Verifying Method Calls: Finally, you use verify to ensure that the spy’s getAllEmployees() method was invoked.

How to Stub Methods in a Mockito Spy?

Stubbing in Mockito allows you to override the behavior of a method by providing a predefined (fake) result. Instead of executing the original method logic, the stubbed method returns the value you specify during the test.

When working with spies, stubbing is useful because spies wrap real objects whose methods are fully functional by default. By stubbing, you can control or bypass specific method calls while leaving the rest of the object’s behavior unchanged.

While both are valid, doReturn(...).when(...) is recommended for spies, because when(...).thenReturn(...) can invoke the real method unintentionally.

In the following example, you’ll see how to use doReturn(...).when(...) to stub a method in a Mockito spy test.

Using doReturn() for Stubbing

When you work with spies in Mockito, you typically use the doReturn().when() syntax for stubbing. Unlike regular mocks, spies call the actual implementation of methods by default. By using doReturn(), you can override a real method’s return value with controlled, fake data during test execution.

Create a new test file as CompaniesSpyStubbingTest.java and type the code below:

package com.alexanie.app.company;

import com.alexanie.app.model.Companies;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Arrays;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

public class CompaniesSpyStubbingTest {

   @Test
   @DisplayName("Spy with doReturn().when()")
   void spyWithDoReturnWhen() {
       // Real object
       Companies realCompany = new Companies(
               2L,
               "DataHub",
               800,
               Arrays.asList("Big Data", "Analytics", "ML")
       );

       // Spy the object
       Companies spyCompany = spy(realCompany);

       // Stubbing with doReturn().when()
       doReturn(2000).when(spyCompany).getNumberOfEmployees();

       // Act
       int employees = spyCompany.getNumberOfEmployees();

       // Assert
       assertThat(employees).isEqualTo(2000);  // Stubbed value
       assertThat(spyCompany.getName()).isEqualTo("DataHub"); // Real value still works

       // Verify interaction
       verify(spyCompany).getNumberOfEmployees();

       System.out.println(employees + " stubbed number of employees");
       // -> 2000 (stubbed number of employees)
   }
}

Code Walkthrough:

  • Stubbing Method Return Value: In the CompaniesSpyStubbingTest, the getNumberOfEmployees() method originally returns 800.
  • Overriding Default Behavior: You use doReturn(2000).when(spyCompany).getNumberOfEmployees() to replace the real return value.
  • Returning Fake Data: The method now returns the fake value 2000 instead of the actual one.
  • Verifying Interactions: This approach helps you test and verify behaviors without depending on the real implementation.

Handling Exceptions With doThrow()

In Mockito, stubbing can also simulate exceptional behavior. The doThrow() method is specifically designed for stubbing void methods so that they throw exceptions during test execution. This is useful for simulating errors like database failures, network outages, or invalid input, and verifying how the system responds.

To demonstrate this, create a new test file named CompaniesSpyExceptionTest.java inside the exception package under your test directory, and type the code below:

package com.alexanie.app.exceptions;

import com.alexanie.app.model.Companies;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.*;

public class CompaniesSpyExceptionTest {

   @Test
   @DisplayName("Spy with doThrow() for exception handling")
   void spyWithDoThrow() {
       // Create a real object
       Companies realCompany = new Companies(
               3L,
               "CloudX",
               500,
               java.util.Arrays.asList("Cloud", "SaaS")
       );

       // Spy the object
       Companies spyCompany = spy(realCompany);

       // Stub a void method to throw an exception
       doThrow(new IllegalStateException("Database connection failed"))
               .when(spyCompany)
               .clearDepartments(); // assume this is a void method in Companies

       // Act & Assert: verify that exception is thrown
       assertThatThrownBy(spyCompany::clearDepartments)
               .isInstanceOf(IllegalStateException.class)
               .hasMessage("Database connection failed");

       // Verify interaction
       verify(spyCompany).clearDepartments();

       System.out.println("Exception successfully stubbed with doThrow()");
   }
}

Code Walkthrough:

  • Stubbing a Void Method: The clearDepartments() method in the Companies class is a void method that’s stubbed using doThrow(new IllegalStateException(...)) to throw an exception instead of running its real logic.
  • Verifying Exception Behavior: Using assertThatThrownBy() from AssertJ, you confirm that the expected exception is thrown with the correct message.
  • Simulating Error Conditions: This ensures your test can emulate failures and verify that the system responds appropriately.

Using doAnswer() for Custom Answers

In Mockito, the doAnswer() method provides a way to define custom behavior when a stubbed method is called. Unlike doReturn() or thenReturn(), which simply return predefined values, doAnswer() gives you full control to execute logic dynamically at runtime based on the method’s arguments or the context of the call.

mockito doanswer

This makes doAnswer() very useful when:

  • Using doAnswer() for Side Effects: You can perform actions like logging, modifying input values, or triggering callbacks when the stubbed method is called.
  • Using doAnswer() for Conditional Behavior: You can make the stubbed method behave differently depending on the input arguments passed during the test.
  • Using doAnswer() for Complex Simulations: You can simulate advanced behaviors that go beyond simple return values, providing more flexibility in your test scenarios.

Let’s look at an example.

Here, you will use Mockito will spy on the Companies model and use doAnswer() to dynamically calculate and return the number of employees.

Create a file as CompaniesSpyDoAnswerTest.java in your company package under your test folder and type the code below.

package com.alexanie.app.company;

import com.alexanie.app.model.Companies;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Arrays;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

public class CompaniesSpyDoAnswerTest {

   @Test
   @DisplayName("Spy with doAnswer() for dynamic behavior")
   void spyWithDoAnswer() {
       // Real object
       Companies realCompany = new Companies(
               1L,
               "TechNova",
               1200,
               Arrays.asList("AI", "Cloud", "Data")
       );

       // Spy the object
       Companies spyCompany = spy(realCompany);

       // Stub getNumberOfEmployees() dynamically with doAnswer()
       doAnswer(invocation -> {
           // Access the real method value
           int original = realCompany.getNumberOfEmployees();
           // Add 100 as a "bonus headcount"
           return original + 100;
       }).when(spyCompany).getNumberOfEmployees();

       // Act
       int employees = spyCompany.getNumberOfEmployees();

       // Assert
       assertThat(employees).isEqualTo(1300); // 1200 original + 100 bonus
       assertThat(spyCompany.getName()).isEqualTo("TechNova"); // Real value works

       // Verify interaction
       verify(spyCompany).getNumberOfEmployees();

       System.out.println("Dynamic employees count: " + employees);
       // -> Dynamic employees count: 1300
   }
}

Code Walkthrough:

  • Creating a Spy on Companies: You create a spy of the Companies object to monitor and control its behavior during testing.
  • Intercepting Calls with doAnswer(): You intercept the getNumberOfEmployees() method call and dynamically customize the return value by adding 100 to the real value.
  • Modifying Original Values: Unlike doReturn(), doAnswer() allows you to access and modify the original return value before sending it back.

This shows how doAnswer() can be a powerful tool for simulating dynamic, real-world scenarios in your Mockito tests.

How to Verify Method Interactions in Mockito Spy?

When working with spies in Mockito, you’re interacting with a real object that has been wrapped by a spy. This means the object’s original methods are available and can be invoked during the test.

However, because spies are designed to track behavior, Mockito allows you to verify whether specific methods on the spied object were invoked, how many times they were called, and with what arguments.

This verification is performed using the verify() method. By applying verify(), you can assert that the interactions with your Mockito spy occurred exactly as expected, ensuring that your business logic calls the correct methods under the right conditions.

In the following example, you’ll learn how to use the verify() method with a Mockito spy to check method calls and interactions during test execution.

Using verify() Method

The verify() method in Mockito is used to validate interactions between a test and its spied object. Specifically, it ensures that a given method was invoked during test execution, and it allows you to assert details such as the number of invocations and the arguments passed.

mockito verify method

This is crucial for testing not just what your code returns, but also how your code behaves in terms of method calls and side effects.

Create a file named CompaniesSpyObjectTest.java in your company package under the test folder:

package com.alexanie.app.company;

import com.alexanie.app.model.Companies;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Arrays;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

public class CompaniesSpyObjectTest {

   @Test
   @DisplayName("Spy on Companies object and override getter")
   void spyOnCompaniesObject() {
       // Create a real Companies instance
       Companies realCompany = new Companies(
               10L,
               "NextGenAI",
               1500,
               Arrays.asList("AI", "Robotics", "Cloud")
       );

       // Wrap it in a spy
       Companies spyCompany = spy(realCompany);

       // Stub one method: override getNumberOfEmployees()
       when(spyCompany.getNumberOfEmployees()).thenReturn(3000);

       // Use spy normally
       String name = spyCompany.getName();
       int employees = spyCompany.getNumberOfEmployees(); // <- overridden

       // Assertions
       assertThat(name).isEqualTo("NextGenAI");   // comes from real object
       assertThat(employees).isEqualTo(3000);     // comes from stubbed spy

       // Verify interactions
       verify(spyCompany).getName();
       verify(spyCompany).getNumberOfEmployees();
   }
}

Code Walkthrough:

  • Using Spies with Real Objects: A real Companies object is wrapped inside a spy using spy(realCompany).
  • Stubbing Specific Methods: The getNumberOfEmployees() method is stubbed to return a fake value (3000), while other methods, such as getName(), continue to use the real implementation.
  • Validating Results: Assertions confirm that the stubbed method returns the overridden value, and the non-stubbed method returns the actual value from the real object.
  • Verifying Interactions: The verify() method checks that both getName() and getNumberOfEmployees() were invoked during the test, ensuring that the interactions occurred exactly as expected.

Using verify() in this way ensures interaction-based testing, which complements state-based testing (via assertions) for more robust and reliable test cases.

Verifying Method Call Counts

In addition to verifying whether a method was called, Mockito also allows you to specify how many times a method should have been invoked on a spied object.

This is especially useful when you want to confirm the exact number of interactions, for example, when a method should only be executed once, multiple times, or never at all.

Mockito provides several verification modes:

  • times(n): Verifies that a method was called exactly n times.
  • never(): Verifies that a method was never invoked.
  • atLeast(n): Verifies that a method was called at least n times.
  • atMost(n): Verifies that a method was called at most n times.

Create a new file CompaniesSpyVerifyCountsTest.java, under your company package in the test folder, and add the following code:

package com.alexanie.app.company;
import com.alexanie.app.model.Companies;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
public class CompaniesSpyVerifyCountsTest {
   @Test
   @DisplayName("Verify method call counts with spy")
   void verifyMethodCallCounts() {
       // Create a real Companies object
       Companies realCompany = new Companies(
               20L,
               "CloudWare",
               2500,
               Arrays.asList("Cloud", "AI", "DevOps")
       );
       // Wrap the object with a spy
       Companies spyCompany = spy(realCompany);
       // Stub one method for demonstration
       when(spyCompany.getNumberOfEmployees()).thenReturn(4000);
       // Interact with the spy
       String name = spyCompany.getName();
       int employees = spyCompany.getNumberOfEmployees();
       int employeesAgain = spyCompany.getNumberOfEmployees();
       // Assertions
       assertThat(name).isEqualTo("CloudWare");
       assertThat(employees).isEqualTo(4000);       // Stubbed value
       assertThat(employeesAgain).isEqualTo(4000);  // Stubbed value

       // Verify call counts
       verify(spyCompany, times(1)).getName();                 // called once
       verify(spyCompany, times(2)).getNumberOfEmployees();    // called twice
       verify(spyCompany, never()).setName("SomethingElse");   // never called
       verify(spyCompany, atLeast(1)).getNumberOfEmployees();  // at least once
       verify(spyCompany, atMost(3)).getNumberOfEmployees();   // no more than 3 times
   }
}

Code Walkthrough:

  • Using times(1) Verification: The getName() method was called once, verified with verify(spyCompany, times(1)).getName().
  • Using times(2)Verification: The getNumberOfEmployees() method was called twice, confirmed with verify(spyCompany, times2).getNumberOfEmployees().
  • Using never() Verification: The setName() method was never called, validated with verify(spyCompany, never()).setName(anyString()).
  • Using atLeast() and atMost() Verification: Applied atLeast(1) and atMost(3) to ensure the method’s invocation count falls within the acceptable range.

This provides a powerful way to validate not only the existence of interactions, but also their frequency, ensuring that your business logic enforces the correct number of method invocations.

Mockito Spy vs Mockito Mock: Key Differences

Both spies and mocks are fundamental features of the Mockito framework, but they serve distinct testing purposes.

A spy wraps a real instance of a class, meaning it retains the original behavior of the object while still allowing selective method stubbing and verification. Spies are useful for partial mocking, where you want the real logic to execute but still control or observe specific behaviors.

In contrast, a mock is a completely fake object created to simulate a dependency without executing any real logic. By default, all method calls on a mock return default values (null, 0, false, etc.) unless explicitly stubbed. Mocks are ideal when you want to fully isolate the system under test from external dependencies.

FeatureMockito SpyMockito Mock
Object TypeReal object wrapped with spying capabilitiesFake/dummy object
Method CallsExecutes real methods by defaultDo not execute real methods by default
Default ReturnExecutes real methods and returns a valueReturns default values (null, 0, etc.)
PurposePartial mocking, verifying real object behaviorIsolate dependencies, replace external resources
ImplementationMockito.spy(realObject)Mockito.mock(Class.class)

Mockito offers a robust framework for mocking and spying in unit testing, helping developers isolate and verify behaviors effectively.

Similarly, in the JavaScript ecosystem, frameworks like Jest, Mocha, and Jasmine provide comparable capabilities for mock creation and test isolation.

To understand how these frameworks differ and how they help perform unit tests, check out this detailed comparison of Jest vs Mocha vs Jasmine.

Troubleshooting Common Issues with Mockito Spy

When working with Mockito spies, you might occasionally run into unexpected exceptions or confusing behavior, especially when mixing real logic with stubbing or verification. Understanding the root cause of these errors can save hours of debugging time.

Below are some of the most common issues and how you can fix them effectively:

NotAMockException

This exception occurs when attempting to use verify(), when(), or other Mockito-specific methods on an object that is neither a mock nor a spy.

Solution: Ensure that the target object is properly created as a mock (Mockito.mock()) or spy (Mockito.spy() or @Spy). Also, confirm that the test class is annotated with @ExtendWith(MockitoExtension.class) to initialize annotations.

WantedButNotInvoked

This happens when verify() expects a method invocation that never occurred, or the actual call was made with arguments different from those specified. Mockito enforces strict argument matching by default.

Solution: Verify that the method was actually called and that the arguments align with your verify() clause. If flexibility is required, use argument matchers like any(), eq(), or argThat().

java: illegal character: '\ufeff' and class, interface, enum, or record expected

A Byte Order Mark (BOM) has been introduced into the source file, typically due to file encoding issues. Java does not accept BOM characters, which results in compiler errors.

Solution: Remove the BOM from the source file by re-saving it in UTF-8 without BOM. Most IDEs (e.g., IntelliJ, Eclipse) provide an option in file encoding settings to "Remove BOM".

UnnecessaryStubbingException

Mockito throws this exception when a method is stubbed but never actually invoked in the test, which indicates redundant or unused stubbing.

Solution: Remove unnecessary stubs to keep the test concise. Alternatively, annotate the test class with @MockitoSettings(strictness = Strictness.LENIENT) if you want to allow unused stubs in exploratory or setup-heavy tests.

NullPointerException in Spy Tests

Unlike mocks, spies invoke real methods by default. If a real method accesses an uninitialized dependency, it may trigger a NullPointerException.

Solution: Stub methods that rely on uninitialized collaborators using doReturn() or doThrow(). For dependencies, ensure proper injection (e.g., with @InjectMocks) to avoid null references.

WrongTypeOfReturnValue

This occurs when stubbing specifies a return value that is incompatible with the method’s declared return type.

Solution: Verify that the stubbed return type matches the method signature. For generic methods, ensure type parameters are correctly resolved.

Argument Matching Failures

Mockito requires either all arguments in a stub/verify call to be exact values or all arguments to use matchers. Mixing raw values with argument matchers (e.g., eq() with a plain string) leads to InvalidUseOfMatchersException.

Solution: Use either argument literals or matchers consistently in the same method call. For example, replace verify(service).save(eq("John"), "Doe") with verify(service).save(eq("John"), eq("Doe")).

SpyOnFinal or CannotMock/Spy Class Errors

By default, Mockito cannot mock or spy on final classes and final methods. Attempting to do so without additional configuration will fail.

Solution: Enable the inline mock maker by adding the mockito-inline dependency. This allows mocking and spying on final classes and methods.

With these troubleshooting strategies, developers can diagnose and resolve the most common pitfalls encountered when working with Mockito spies, leading to more stable and maintainable test suites.

Best Practices for Using Mockito Spy

When working with spies in Mockito, it’s important to follow certain best practices to ensure your tests are reliable, maintainable, and easy to understand.

Below are key recommendations:

  • Prefer doReturn(), doAnswer(), and doThrow() for Stubbing: When stubbing methods on spies, use doReturn(), doAnswer(), or doThrow() instead ofwhen().thenReturn(). This avoids unintentional invocation of the real method during stubbing and ensures better control of test behavior.
  • Always Verify Interactions: Use verify() to confirm that specific methods were called on the spy. Place verification statements at the end of the test so assertions about behavior are checked only after all test actions are complete. This improves test accuracy and readability.
  • Use @DisplayName for Readability: Annotating test methods with @DisplayName improves clarity when reading test reports. Descriptive labels make it easier to understand the intent of each test, especially in larger codebases.
  • Avoid Over-Spying Complex Objects: Do not spy on large or complex objects with many dependencies. Over-spying can create fragile and tightly coupled tests that break easily when the underlying code changes. Keep your spy usage focused and minimal.
  • Use @Spy Annotation for Cleaner Code: Instead of manually creating spies with Mockito.spy(), use the @Spy annotation when working with JUnit and MockitoExtension. This reduces boilerplate and makes your test setup cleaner and more concise.
  • Avoid Side Effects in Spies: Be cautious when stubbing methods that cause side effects (e.g., database writes, external API calls). If those methods are invoked accidentally, they could lead to unexpected behavior. Use stubbing to neutralize or replace side-effect-heavy methods.
  • Keep Stubbing Targeted: Only stub the methods you need for your test scenario. Over-stubbing can make tests harder to follow and hide issues with real implementations. Targeted stubbing keeps tests precise and intentional.
  • Verify Call Counts Where Relevant: Use verification modes like times(n), never(), or atLeastOnce() to assert not just that a method was called, but also how many times it was invoked. This helps catch redundant or missing method calls in your code.
  • Favor Constructor Injection Over Field Injection: When combining @InjectMocks and @Spy, prefer constructor injection for dependencies. This makes dependencies explicit, reduces hidden wiring, and improves test robustness.
  • Use Custom Answers for Complex Logic: For advanced scenarios, prefer doAnswer() when you need dynamic behavior, such as returning values based on method arguments. This is more flexible than static doReturn() stubbing and better reflects real-world logic.
  • Run Tests in Parallel for Faster Feedback: Parallel execution in Mockito testing accelerates feedback loops by allowing multiple test cases to run simultaneously. In large-scale Java unit testing projects, ensure mocks, stubs, and shared resources are thread-safe to prevent race conditions or data conflicts.
  • To further optimize performance, you can run your Mockito-based JUnit tests on cloud platforms like LambdaTest, which support online JUnit testing. This enables parallel execution across multiple environments, shortens build times, and ensures stable, consistent results in CI/CD pipelines.

...

Conclusion

Mockito spies strike the perfect balance between realism and control in unit testing. They enable you to test actual object behavior while still giving you the flexibility to stub or verify specific methods when needed. When used correctly, spies simplify partial mocking, improve test accuracy, and make debugging easier.

While mocks are best suited for isolating dependencies, spies shine in scenarios where real logic needs to be executed. By combining best practices such as using doReturn() for stubbing, @Spy for cleaner setup, and proper verification placement, you can ensure cleaner and more maintainable test suites.

In essence, understanding when to use a mock versus a spy helps you design more reliable, readable, and effective tests, ultimately strengthening the overall quality of your codebase.

Frequently Asked Questions (FAQs)

When should you use a Mockito Spy instead of a Mock?
Use a spy when you need to test a real object but want to override or verify specific methods. Spies wrap real instances, allowing partial mocking while letting other methods execute normally.
Can you spy on private methods in Mockito?
Mockito cannot spy or mock private methods directly. Test private logic indirectly via public methods or refactor complex private methods into testable classes or package-private methods.
Why does a spy sometimes execute real methods unintentionally?
Spies call real methods by default. Using when() instead of doReturn()/doAnswer() may trigger the real method prematurely, causing unwanted side effects like state changes or external calls.
How do you prevent NullPointerExceptions when using spies?
Ensure all dependencies are initialized before spying. Alternatively, stub methods relying on uninitialized fields with doReturn() or doThrow() to avoid executing real logic that may cause NullPointerExceptions.
What is the best way to verify method invocations on a spy?
Use verify() to confirm method calls, invocation count, and arguments. Options like times(), never(), or atLeastOnce() provide precise verification, performed after test logic for accurate results.
Why do some tests fail with a WantedButNotInvoked exception?
This occurs when an expected method is not called due to wrong arguments, missing calls, or incorrect object references. Use matchers like eq() or any() to ensure verification matches the actual invocation.
Can you use Mockito spies in multithreaded tests?
Yes, but carefully. Spies inherit the thread-safety of real objects. For non-thread-safe objects, use separate spies per thread or synchronize access to avoid race conditions and inconsistent verification.
What should you do if a method on a spy returns the wrong value?
If a stubbed method returns the real value, ensure doReturn(value).when(spy).method() is used instead of when(spy.method()).thenReturn(value), which calls the real method immediately.
Can spies be used effectively with legacy codebases?
Yes, spies allow testing parts of a class without major refactoring. Stub only specific methods that are hard to isolate while letting the rest behave normally, enabling validation of legacy logic safely.
Why is it important not to overuse spies in unit testing?
Overusing spies makes tests fragile and dependent on implementation details. Focus on mocking dependencies and verifying outcomes, using spies only when real behavior must be observed and cannot be tested otherwise.

Did you find this page helpful?

Helpful

NotHelpful

More Related Hubs

ShadowLT Logo

Start your journey with LambdaTest

Get 100 minutes of automation test minutes FREE!!