0
0
JUnittesting~15 mins

@Spy for partial mocking in JUnit - Deep Dive

Choose your learning style9 modes available
Overview - @Spy for partial mocking
What is it?
@Spy is a feature in testing frameworks like Mockito used with JUnit to create partial mocks of real objects. It allows you to call real methods on an object while still being able to stub or verify specific method calls. This means you can test parts of an object’s behavior without fully replacing it with a fake. It is useful when you want to test real logic but control or observe some interactions.
Why it matters
Without @Spy, you would either have to test the entire real object with all its dependencies or fully mock it, losing real behavior. This can make tests fragile or less meaningful. @Spy solves this by letting you mix real and mocked behavior, making tests more flexible and focused. It helps catch bugs in real code while isolating parts that are hard to test or slow.
Where it fits
Before learning @Spy, you should understand basic unit testing, mocking with @Mock, and how to write JUnit tests. After mastering @Spy, you can explore advanced mocking techniques like argument captors, verifying call order, and integration testing with mocks.
Mental Model
Core Idea
@Spy lets you wrap a real object so you can use its real methods but still control or check some methods like a mock.
Think of it like...
Imagine a car mechanic who drives a real car but can replace or watch specific parts like the brakes or engine to test them separately without changing the whole car.
┌───────────────┐
│   Real Object │
│  (full logic) │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│    @Spy Proxy │
│  (partial mock)│
└──────┬────────┘
       │
  ┌────┴─────┐
  │ Real call│
  │ or stub  │
  └──────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding basic mocking
🤔
Concept: Learn what mocking means: replacing real objects with fake ones to isolate tests.
In unit testing, mocking means creating a fake version of a class that returns preset values or records calls. For example, if you have a Calculator class, you can mock it to always return 5 for addition, ignoring real logic.
Result
You can test code that depends on Calculator without running its real code.
Understanding mocking is essential because @Spy builds on this idea but adds real behavior.
2
FoundationWhat is partial mocking?
🤔
Concept: Partial mocking means mixing real method calls with mocked ones on the same object.
Instead of fully replacing an object, partial mocking lets some methods run real code while others are stubbed. This helps when you want to test real logic but control or observe some parts.
Result
Tests become more flexible and realistic, focusing on what matters.
Knowing partial mocking helps you avoid over-mocking and keeps tests meaningful.
3
IntermediateUsing @Spy annotation in JUnit
🤔Before reading on: do you think @Spy creates a new object or wraps an existing one? Commit to your answer.
Concept: @Spy creates a proxy around a real object, allowing real methods to be called unless stubbed.
In JUnit with Mockito, you declare @Spy on a field. Mockito creates a spy object that calls real methods by default. You can stub specific methods to return fake values or verify calls.
Result
You get an object that behaves like the real one but can be controlled in tests.
Understanding that @Spy wraps the real object explains why unstubbed methods run real code.
4
IntermediateStubbing methods on a @Spy object
🤔Before reading on: do you think stubbing a method on a spy replaces the real method permanently or only for the test? Commit to your answer.
Concept: You can replace specific methods on a spy with stubbed behavior while keeping others real.
Using Mockito.when(spy.method()).thenReturn(value) replaces that method's output. Other methods still run real code. This lets you isolate parts that are hard to test or slow.
Result
Tests can focus on specific behaviors without losing real logic elsewhere.
Knowing how to stub selectively prevents over-mocking and keeps tests efficient.
5
IntermediateVerifying interactions on @Spy objects
🤔
Concept: You can check if certain methods were called on the spy during the test.
Mockito.verify(spy).method() lets you confirm that a method was called. This helps ensure your code interacts with the object as expected, combining real behavior with verification.
Result
You get confidence that your code uses the object correctly.
Verifying calls on spies helps catch bugs in how your code uses real objects.
6
AdvancedCommon pitfalls with @Spy usage
🤔Before reading on: do you think @Spy automatically initializes the real object or do you need to create it yourself? Commit to your answer.
Concept: Using @Spy incorrectly can cause null pointers or unexpected behavior if the real object is not initialized properly.
You must create the real object before spying or use MockitoAnnotations.openMocks(this) to initialize. Forgetting this leads to errors because the spy wraps a null reference.
Result
Proper initialization avoids test failures and confusion.
Knowing initialization details prevents common errors that waste time debugging.
7
ExpertHow @Spy affects test design and maintenance
🤔Before reading on: do you think overusing @Spy makes tests simpler or more complex? Commit to your answer.
Concept: @Spy can make tests more realistic but also harder to maintain if overused or misused.
While @Spy helps test real behavior, relying too much on it can create fragile tests that break when internal logic changes. Experts balance @Spy use with clear test boundaries and prefer pure mocks when isolation is critical.
Result
Tests remain reliable, fast, and easy to understand.
Understanding tradeoffs of @Spy use helps write maintainable tests and avoid hidden dependencies.
Under the Hood
@Spy creates a proxy object that holds a reference to the real object. When a method is called, the proxy checks if the method is stubbed. If yes, it returns the stubbed value. If not, it forwards the call to the real object's method. This proxy also records method calls for verification. Internally, Mockito uses bytecode manipulation or dynamic proxies to create this wrapper at runtime.
Why designed this way?
This design allows tests to combine real behavior with controlled stubbing and verification without rewriting or subclassing the original class. It balances flexibility and simplicity. Alternatives like full mocks lose real logic, and subclassing is cumbersome and less dynamic.
┌───────────────┐       ┌───────────────┐
│   Test Code   │──────▶│   @Spy Proxy  │
└───────────────┘       └──────┬────────┘
                                │
               ┌────────────────┴───────────────┐
               │                                │
       ┌───────▼───────┐                ┌───────▼───────┐
       │ Stubbed Method│                │ Real Method   │
       └───────────────┘                └───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does @Spy create a new object or wrap an existing one? Commit to your answer.
Common Belief:People often think @Spy creates a brand new mock object with no real behavior.
Tap to reveal reality
Reality:@Spy wraps an existing real object and calls real methods unless stubbed.
Why it matters:Misunderstanding this leads to tests that expect no real logic but get unexpected real method calls, causing flaky tests.
Quick: Does stubbing a method on a spy affect all tests or just the current one? Commit to your answer.
Common Belief:Some believe stubbing on a spy permanently changes the method for all tests.
Tap to reveal reality
Reality:Stubbing only affects the current test instance; each test gets a fresh spy.
Why it matters:Thinking stubs are permanent can cause confusion about test isolation and lead to incorrect debugging.
Quick: Can you use @Spy without initializing the real object? Commit to your answer.
Common Belief:Many assume @Spy automatically creates and initializes the real object.
Tap to reveal reality
Reality:@Spy requires the real object to be created before spying or proper initialization via MockitoAnnotations.
Why it matters:Failing to initialize causes null pointer exceptions and wasted debugging time.
Quick: Does using @Spy always make tests better? Commit to your answer.
Common Belief:Some think @Spy is always the best choice for testing real objects.
Tap to reveal reality
Reality:Overusing @Spy can make tests fragile and harder to maintain compared to pure mocks or integration tests.
Why it matters:Misusing @Spy leads to brittle tests that break with internal changes, increasing maintenance cost.
Expert Zone
1
Using @Spy on final classes or methods requires additional configuration or is unsupported, which can surprise many.
2
Combining @Spy with @InjectMocks can cause subtle initialization order issues that affect test behavior.
3
Mockito’s internal bytecode manipulation for @Spy can interfere with other frameworks that modify classes, requiring careful setup.
When NOT to use
Avoid @Spy when you want pure unit tests with full isolation; use @Mock instead. Also, do not use @Spy for complex objects with many dependencies that should be tested via integration tests.
Production Patterns
In real projects, @Spy is used to test legacy code where refactoring is hard, allowing partial control without rewriting. It is also used to verify side effects on real objects while stubbing slow or external calls.
Connections
Proxy Pattern (Software Design)
@Spy uses the proxy pattern to wrap real objects and control method calls.
Understanding proxies in design helps grasp how @Spy intercepts calls and decides between real or stubbed behavior.
Aspect-Oriented Programming (AOP)
Both @Spy and AOP intercept method calls to add behavior without changing original code.
Knowing AOP concepts clarifies how @Spy can inject testing logic dynamically around real methods.
Quality Control in Manufacturing
Partial mocking is like inspecting parts of a product while letting the rest function normally.
This cross-domain link shows how testing focuses on critical parts without discarding the whole system.
Common Pitfalls
#1Null pointer error due to uninitialized real object
Wrong approach:@Spy MyClass myClassSpy; @Test void test() { when(myClassSpy.someMethod()).thenReturn(5); // test code }
Correct approach:@Spy MyClass myClassSpy = new MyClass(); @Before void setup() { MockitoAnnotations.openMocks(this); } @Test void test() { when(myClassSpy.someMethod()).thenReturn(5); // test code }
Root cause:The spy wraps a null reference because the real object was never created or initialized.
#2Stubbing a method incorrectly on a spy causing real method call
Wrong approach:when(myClassSpy.someMethod()).thenReturn(5); // causes real method call and exception
Correct approach:doReturn(5).when(myClassSpy).someMethod(); // avoids calling real method during stubbing
Root cause:Using when().thenReturn() on spies calls the real method during stubbing, which can cause errors.
#3Overusing @Spy leading to fragile tests
Wrong approach:Using @Spy on many classes and stubbing many methods without clear test boundaries.
Correct approach:Use @Spy sparingly; prefer @Mock for isolation and integration tests for full behavior.
Root cause:Misunderstanding when to use partial mocks causes complex, brittle tests that break easily.
Key Takeaways
@Spy creates a proxy around a real object to mix real method calls with mocked behavior.
It helps test real logic while controlling or verifying specific methods, improving test flexibility.
Proper initialization of the real object is essential to avoid null pointer errors with @Spy.
Overusing @Spy can make tests fragile; balance its use with pure mocks and integration tests.
Understanding @Spy’s proxy mechanism connects to broader software design patterns like proxies and AOP.