# Spies, Static and Final Mocking — JUnit 5 & Mockito

Source: https://www.skillbyai.com/en/java-testing/v-spies

> Use spies for partial mocking and mock static methods and final classes when unavoidable.

## Powerful tools, used sparingly

A **spy** (`spy(realObject)` or `@Spy`) wraps a **real object**: unstubbed methods run the real code, while selected methods can be stubbed and all calls can be verified. Spies help with legacy code that is hard to restructure, but needing them often signals that the class does too much. Stub spies with **`doReturn(x).when(spy).method()`**, because `when(spy.method())` would call the real method during stubbing. With Mockito 5's default **inline mock maker**, you can also mock **final** classes and methods and, with **`Mockito.mockStatic(Type.class)`**, **static methods** inside a try-with-resources block that limits the scope. Use these features to cope with code you cannot change (third-party utilities, legacy statics), not as a design default: static calls such as `LocalDate.now()` or `UUID.randomUUID()` inside business logic are better replaced by injected collaborators (`Clock`, an ID generator). On recent JDKs, Mockito recommends configuring it as a **Java agent** in the build so the inline mock maker does not rely on dynamic agent loading, which newer JDKs warn about.

## A spy, a static mock and a better design

mockStatic is scoped; injecting a Clock removes the need for it.

```java
@Test
void spyStubsOneMethodOnly() {
    List<String> real = new ArrayList<>(List.of("a", "b"));
    List<String> spy = spy(real);
    doReturn(100).when(spy).size();           // stub one method; others are real
    spy.add("c");
    assertThat(spy.size()).isEqualTo(100);
    assertThat(spy.get(2)).isEqualTo("c");
}

@Test
void staticMockWhenCodeCannotChange() {
    try (MockedStatic<LegacyIds> ids = mockStatic(LegacyIds.class)) {
        ids.when(LegacyIds::next).thenReturn("ID-42");
        assertThat(new LegacyInvoiceFactory().create().id()).isEqualTo("ID-42");
    }   // static behaviour restored here
}

// better design: inject time instead of calling LocalDate.now() inside the logic
final class DueDateCalculator {
    private final Clock clock;
    DueDateCalculator(Clock clock) { this.clock = clock; }
    LocalDate dueDate(int days) { return LocalDate.now(clock).plusDays(days); }
}

@Test
void dueDateIsDeterministicWithFixedClock() {
    Clock fixed = Clock.fixed(Instant.parse("2026-10-03T00:00:00Z"), ZoneOffset.UTC);
    assertThat(new DueDateCalculator(fixed).dueDate(30)).isEqualTo(LocalDate.of(2026, 11, 2));
}
```

## Inject Clock for time-based logic

Code that calls `LocalDate.now()` directly produces tests that fail at midnight or month end. Passing a `java.time.Clock` makes time a normal dependency you can fix in tests.

**Quiz:** Why use doReturn(...).when(spy).method() instead of when(spy.method()).thenReturn(...) for spies?

- [ ] doReturn is faster
- [ ] when() does not work with Mockito 5
- [ ] doReturn verifies the call
- [x] when(spy.method()) calls the real method during stubbing, which may have side effects or fail

*Answer:* when(spy.method()) calls the real method during stubbing, which may have side effects or fail. Spies run real code, so the doReturn style avoids executing the real method while stubbing.
