0
0
JUnittesting~15 mins

Spy objects in JUnit - Deep Dive

Choose your learning style9 modes available
Overview - Spy objects
What is it?
Spy objects are special test doubles that record how real objects are used during a test. They let you check if certain methods were called, with what arguments, and how many times. Unlike mocks that replace behavior, spies wrap real objects to observe their actions while still running actual code. This helps verify interactions without losing real functionality.
Why it matters
Spy objects exist to help testers confirm that parts of the code interact correctly without changing the real behavior. Without spies, you might miss bugs where methods are not called as expected or called incorrectly. This can lead to hidden errors in complex systems where behavior depends on correct communication between components. Spies make tests more reliable and meaningful by combining observation with real execution.
Where it fits
Before learning about spy objects, you should understand basic unit testing and test doubles like stubs and mocks. After mastering spies, you can explore advanced mocking frameworks, behavior verification, and integration testing. Spies fit in the middle of the testing journey, bridging simple fake objects and full mocks.
Mental Model
Core Idea
A spy object is a real object wrapped to watch and record its method calls during tests without changing its behavior.
Think of it like...
Imagine a security camera installed in a store that records what customers do but doesn't interfere with their shopping. The camera lets you review actions later without changing the shopping experience.
┌───────────────┐
│   Test Code   │
└──────┬────────┘
       │ calls methods
       ▼
┌───────────────┐
│   Spy Object  │
│ (wraps real)  │
└──────┬────────┘
       │ forwards calls
       ▼
┌───────────────┐
│ Real Object   │
│ (actual code) │
└───────────────┘

Spy records calls while real object runs actual code.
Build-Up - 7 Steps
1
FoundationUnderstanding Test Doubles Basics
🤔
Concept: Learn what test doubles are and why they help in testing.
Test doubles are fake objects used in tests to replace real parts of the system. They help isolate the code under test by simulating dependencies. Common types include stubs (provide canned responses) and mocks (verify interactions).
Result
You know why and when to replace real objects in tests to control behavior and verify calls.
Understanding test doubles is essential because spies are a special kind of test double that combines real behavior with observation.
2
FoundationDifference Between Mocks and Spies
🤔
Concept: Distinguish mocks that replace behavior from spies that wrap real objects.
Mocks simulate objects and define expected calls before the test runs, often replacing real code. Spies wrap real objects, letting their real methods run while recording calls. This means spies keep actual behavior but add observation.
Result
You can tell when to use a mock (replace behavior) versus a spy (observe real behavior).
Knowing this difference prevents confusion and helps choose the right tool for verifying interactions.
3
IntermediateCreating a Spy in JUnit with Mockito
🤔Before reading on: do you think a spy requires you to write all method behaviors manually or can it use real methods automatically? Commit to your answer.
Concept: Learn how to create a spy object in JUnit tests using Mockito framework.
In Mockito, you create a spy by calling Mockito.spy(realObject). This wraps the real object so its methods run normally but calls are recorded. For example: MyClass real = new MyClass(); MyClass spy = Mockito.spy(real); Now, calling spy.someMethod() runs real someMethod() but also records the call.
Result
You can create spy objects that behave like real objects but allow verification of method calls.
Understanding that spies automatically call real methods unless stubbed helps avoid unexpected test behavior.
4
IntermediateVerifying Interactions with Spy Objects
🤔Before reading on: do you think verifying calls on spies is done the same way as on mocks, or is it different? Commit to your answer.
Concept: Learn how to check if methods were called on spy objects during tests.
Mockito lets you verify calls on spies using verify(spy). For example: verify(spy).someMethod("arg"); This checks if someMethod was called with "arg". You can also check call counts: verify(spy, times(2)).someMethod(any()); This confirms interaction happened as expected.
Result
You can assert that real methods were called correctly during tests using spies.
Knowing verification works the same way on spies as mocks simplifies test writing and improves confidence in interaction correctness.
5
IntermediateStubbing Methods on Spy Objects
🤔Before reading on: do you think stubbing a method on a spy replaces the real method or just adds extra behavior? Commit to your answer.
Concept: Learn how to override specific methods on spies to control their behavior.
Sometimes you want a spy to run real methods except for one method you want to stub. Use Mockito.doReturn(value).when(spy).method(args) to stub without calling real method. For example: doReturn(10).when(spy).calculate(); This makes calculate() return 10 instead of running real code.
Result
You can selectively override spy methods to control test scenarios while keeping other real behavior.
Understanding stubbing on spies prevents accidental real method calls that may cause side effects or slow tests.
6
AdvancedAvoiding Common Spy Pitfalls
🤔Before reading on: do you think spying on null or uninitialized objects works fine or causes errors? Commit to your answer.
Concept: Learn common mistakes and how to avoid them when using spies in tests.
Spying on null or uninitialized objects causes NullPointerExceptions. Also, stubbing spy methods with when(spy.method()).thenReturn() calls real method first, which can cause side effects. Use doReturn().when() syntax to avoid this. Example mistake: when(spy.method()).thenReturn(value); // calls real method! Correct way: doReturn(value).when(spy).method(); Also, avoid spying on final classes or methods as Mockito may not support it.
Result
You write safer spy tests that avoid crashes and unintended behavior.
Knowing these pitfalls helps maintain stable tests and prevents confusing errors during test runs.
7
ExpertSpy Objects in Complex Integration Tests
🤔Before reading on: do you think spies are only useful in unit tests or can they help in integration tests too? Commit to your answer.
Concept: Explore how spies can be used in larger tests to observe real component interactions without full mocks.
In integration tests, spies let you observe real service or DAO calls while running actual code. This helps verify that components communicate correctly without replacing behavior. For example, spying on a database access object lets you check queries executed while still hitting a test database. This balances realism and observability.
Result
You can write integration tests that verify real interactions without losing control or visibility.
Understanding spy use beyond unit tests unlocks powerful testing strategies for complex systems.
Under the Hood
Spy objects work by creating a proxy around the real object. This proxy intercepts method calls, records details like method name and arguments, then forwards the call to the real object. The proxy uses reflection or bytecode manipulation to wrap methods dynamically at runtime. This allows spies to observe calls without changing the original object's code or behavior.
Why designed this way?
Spies were designed to combine the benefits of real object behavior with the ability to verify interactions. Earlier test doubles either replaced behavior completely or provided no observation. Spies fill the gap by wrapping real objects, enabling more realistic tests and easier debugging. The proxy approach was chosen for flexibility and minimal intrusion.
┌─────────────────────────────┐
│        Test Code            │
└─────────────┬───────────────┘
              │ calls method
              ▼
┌─────────────────────────────┐
│        Spy Proxy            │
│  - Records call details     │
│  - Forwards call to real    │
└─────────────┬───────────────┘
              │ invokes real method
              ▼
┌─────────────────────────────┐
│       Real Object           │
│  - Executes actual code     │
└─────────────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does stubbing a spy method with when(spy.method()).thenReturn() call the real method first? Commit to yes or no.
Common Belief:Stubbing a spy method with when().thenReturn() does not call the real method.
Tap to reveal reality
Reality:Using when(spy.method()).thenReturn() calls the real method immediately, which can cause side effects or errors.
Why it matters:This can cause tests to fail unexpectedly or run slowly due to unwanted real method execution during stubbing.
Quick: Can you spy on a null object without errors? Commit to yes or no.
Common Belief:You can create a spy on any object, even if it is null or uninitialized.
Tap to reveal reality
Reality:Spying on a null or uninitialized object causes NullPointerException errors.
Why it matters:This leads to confusing test failures and wasted debugging time.
Quick: Are spies always better than mocks for verifying interactions? Commit to yes or no.
Common Belief:Spies are always better than mocks because they use real behavior and record calls.
Tap to reveal reality
Reality:Spies are not always better; mocks are preferable when you want to fully control behavior or isolate tests completely.
Why it matters:Using spies when mocks are better can cause tests to be slower, flaky, or harder to maintain.
Quick: Does verifying a method call on a spy check the actual method execution or just the call record? Commit to your answer.
Common Belief:Verifying a method call on a spy only checks if the method was called, not if it executed correctly.
Tap to reveal reality
Reality:Verification on spies confirms the method was called, but the real method also executed, so side effects happen.
Why it matters:This means tests can pass verification but still fail due to real method side effects if not handled carefully.
Expert Zone
1
Spies can cause subtle side effects because real methods run; experts carefully stub or isolate methods to avoid this.
2
Mockito's doReturn().when() syntax is essential for stubbing spies without triggering real method calls, a detail often missed.
3
Spying on partial objects (partial mocks) can lead to brittle tests if internal state changes unexpectedly during real method calls.
When NOT to use
Avoid spies when you need full control over behavior or want to isolate tests completely; use mocks instead. Also, do not use spies on final classes or methods unsupported by your mocking framework. For pure behavior verification without side effects, mocks are safer.
Production Patterns
In production, spies are used to verify real interactions in service layers or DAOs during integration tests. They help confirm that components call each other correctly without replacing real logic. Spies also assist in debugging by recording call sequences in complex workflows.
Connections
Proxy Pattern (Software Design)
Spy objects implement the proxy pattern to intercept and forward method calls.
Understanding the proxy pattern clarifies how spies wrap real objects dynamically to add behavior without changing original code.
Observability in Systems Engineering
Spies provide observability into object interactions during tests, similar to monitoring in systems.
Knowing observability principles helps appreciate how spies reveal internal behavior without interference.
Security Cameras (Physical Surveillance)
Spies act like security cameras that watch real activity without interfering.
This connection shows how non-intrusive observation can provide valuable insights while preserving normal operation.
Common Pitfalls
#1Stubbing spy methods using when(spy.method()).thenReturn() causing real method call.
Wrong approach:when(spy.calculate()).thenReturn(10);
Correct approach:doReturn(10).when(spy).calculate();
Root cause:Misunderstanding that when().thenReturn() calls the real method immediately on spies.
#2Creating a spy on a null object leading to NullPointerException.
Wrong approach:MyClass spy = Mockito.spy(null);
Correct approach:MyClass real = new MyClass(); MyClass spy = Mockito.spy(real);
Root cause:Not initializing the real object before spying on it.
#3Using spies when mocks are more appropriate, causing slow or flaky tests.
Wrong approach:Spy real service objects in all unit tests regardless of side effects.
Correct approach:Use mocks to fully control behavior and isolate tests when real method execution is unnecessary or risky.
Root cause:Confusing the purpose of spies and mocks and not considering test isolation needs.
Key Takeaways
Spy objects wrap real objects to observe method calls while running actual code, combining real behavior with verification.
Creating spies in JUnit with Mockito is simple but requires careful stubbing to avoid unintended real method calls.
Verifying interactions on spies works like mocks but remember real methods execute, which can cause side effects.
Avoid spying on null objects and use doReturn().when() syntax to stub spy methods safely.
Spies are powerful for integration tests and debugging but are not always the best choice compared to mocks.