Keywords: Mockito | Unit Testing | Mock Objects | Spy Objects | Java Testing
Abstract: This article provides an in-depth exploration of the core distinctions between Mock and Spy objects in the Mockito testing framework, illustrated through practical examples. We analyze a common misconception among developers—attempting to use Mock objects to test the real behavior of partial methods within a class—and demonstrate that Spy objects are the correct solution. The article explains the complete simulation nature of Mock objects versus the partial simulation capability of Spy objects, with detailed code examples showing how to properly use Spy to test specific methods while simulating the behavior of other dependent methods. Additionally, we discuss best practices, including the principle of mocking dependencies rather than the class under test itself.
Core Conceptual Differences Between Mock and Spy
In the Mockito testing framework, Mock objects and Spy objects represent two distinct simulation strategies, and understanding their differences is crucial for writing effective unit tests.
A Mock object is a fully simulated instance where all methods are stubbed by default. This means that when you create a Mock object, any method call on that object will not execute the original implementation but instead return "smart return values" (such as null, 0, or empty collections). Unless you explicitly define behavior for specific methods (using Mockito.when()), calling any method on a Mock object will have no actual effect.
In contrast, a Spy object is a wrapper around a real object that retains the original class's actual functionality. You can think of a Spy as a "partial mock"—its methods execute normally by default, but you can selectively override the behavior of certain methods. This makes Spy particularly suitable for testing specific methods within a class while simulating other methods that might involve external dependencies (such as database access).
Common Pitfalls and Solutions
Many developers encounter a typical issue when using Mockito: they want to test one method in a class while forcing other methods in the same class to return predefined simulated values, to avoid external dependencies during testing. Intuitively, they might try to achieve this using Mock objects, but this approach is destined to fail.
Consider the following scenario: you have a MyProcessingAgent class containing a method methodThatNeedsTestCase() that needs testing, and another method otherMethod() that might involve database operations. If you use a Mock object:
MyProcessingAgent mockMyAgent = Mockito.mock(MyProcessingAgent.class);
Mockito.when(mockMyAgent.otherMethod(Mockito.any())).thenReturn(requiredReturnArg);
List myReturnValue = mockMyAgent.methodThatNeedsTestCase();In this case, methodThatNeedsTestCase() will never execute because all methods of a Mock object are stubbed by default. Even if you set a return value for otherMethod(), methodThatNeedsTestCase() itself remains a stubbed method and will not invoke the original implementation.
The correct solution is to use a Spy object:
MyProcessingAgent spyMyAgent = Mockito.spy(new MyProcessingAgent());
Mockito.when(spyMyAgent.otherMethod(Mockito.any())).thenReturn(requiredReturnArg);
List myReturnValue = spyMyAgent.methodThatNeedsTestCase();This way, methodThatNeedsTestCase() executes its original code normally, while otherMethod() returns your predefined simulated value. This allows you to isolate and test the target method while controlling the behavior of its dependent methods.
Practical Code Example Analysis
Let's clarify this concept with a more concrete example. Suppose we have a simple test class:
static class TestClass {
public String getThing() {
return "Thing";
}
public String getOtherThing() {
return getThing();
}
}We want to test the getOtherThing() method but have getThing() return a specific simulated value. The correct approach using Spy is as follows:
public static void main(String[] args) {
final TestClass testClass = Mockito.spy(new TestClass());
Mockito.when(testClass.getThing()).thenReturn("Some Other thing");
System.out.println(testClass.getOtherThing());
}The output of this code will be "Some Other thing". This is because getOtherThing() executes normally and calls getThing(), but the latter has been overridden to return the simulated value.
If we incorrectly use a Mock object:
final TestClass mockClass = Mockito.mock(TestClass.class);
Mockito.when(mockClass.getThing()).thenReturn("Some Other thing");
System.out.println(mockClass.getOtherThing());The output will be null, because getOtherThing() as a method of a Mock object returns null by default and does not execute any actual code.
Best Practices and Considerations
While Spy is very useful in certain scenarios, the best practice in test design is usually to mock dependencies rather than the class under test itself. This means you should aim to pass external dependencies to the class via constructors, setter methods, or dependency injection, and then mock these dependencies in tests, rather than mocking methods of the class under test.
For example, if MyProcessingAgent depends on a DatabaseService, a better approach is:
DatabaseService mockDbService = Mockito.mock(DatabaseService.class);
MyProcessingAgent agent = new MyProcessingAgent(mockDbService);
Mockito.when(mockDbService.queryData(Mockito.any())).thenReturn(mockData);
List result = agent.methodThatNeedsTestCase();This approach makes tests clearer by explicitly indicating the class's dependencies and avoiding testing the internal implementation details of the class.
However, when dealing with legacy code or classes that cannot be easily refactored, Spy offers a practical compromise. When using Spy, keep the following points in mind:
- Spy objects are created based on real objects, so ensure the object can be instantiated correctly (e.g., avoid complex operations in the constructor).
- Spy may not work properly for final methods, static methods, or private methods, as these typically cannot be overridden.
- Overuse of Spy might indicate design issues in the class, such as high coupling or lack of clear dependency injection.
In Mockito, the Mockito.spy() method has two common usages: instance-based Spy (Mockito.spy(new MyClass())) and class-based Spy (Mockito.spy(MyClass.class)). The latter can lead to unexpected behavior in some cases, especially when the class has complex constructors, so instance-based Spy is generally recommended.
Conclusion
Understanding the difference between Mock and Spy in Mockito is key to writing effective unit tests. Mock objects are suitable for fully simulating external dependencies, while Spy objects allow you to override specific methods while retaining partial functionality of the class. When testing a single method within a class that needs to execute normally while simulating other methods it calls, Spy is the correct choice. However, in the long term, designing tests by mocking dependencies rather than the class under test itself often leads to more robust and maintainable test code. Developers should choose the appropriate simulation strategy based on the specific context and follow best practices such as Test-Driven Development (TDD) and dependency injection to improve code quality and testability.