Study Notes
Spring Boot Frameworkapplied-skills

Test Doubles & Mockito

Testing Spring code in isolation — the family of test doubles (dummy, stub, spy, mock, fake), and Mockito for stubbing collaborators, verifying interactions, and writing fast unit tests. Includes interview Q&A.

35 min read·verified Spring Boot 4.x·reviewed 2026-08-28

After this you should be able to

  • Distinguish test doubles
  • Isolate a unit with Mockito
MockitoStub / Spy / MockUnit Testing

Core Java · Unit Testing

How to test a class whose real dependencies are slow, remote, or out of your control — by standing in fakes, stubs, and mocks in their place.

Updated August 2026 · Read ~20 min · Java-301 prep + Mockito Byte


Choose your route#

Problem: a test should reveal one unit’s behaviour without a database or network deciding its speed. Plan 35 minutes for a small Mockito exercise or 15 minutes for interview practice.

Learning goal

Recall first: name the dependency of the class under test; prior constructor injection helps but is not a gate.

  • Distinguish test doubles.
  • Isolate a unit with Mockito.
  • Verify meaningful collaboration.

Shared core: control the collaborator, not the unit#

A dummy fills a parameter, a stub supplies a response, a fake has a working lightweight implementation, a spy observes a real object, and a mock verifies an interaction. The test chooses the smallest double that makes the behaviour observable.

Closest confusion: stubbing controls what a dependency returns; verifying checks that an important message was sent. A mock is not automatically better than state-based assertions.

Failure trace: stubbing findById(1L) then calling findById(2L) silently returns Mockito’s default, often producing a misleading null failure.

Decision rule: mock at a boundary you do not own, assert output when it expresses the contract, and verify interaction only when the interaction itself matters.

Version note: examples use current Mockito style reviewed with Spring Boot 4.x material on 2026-08-28.

Learn deeply: unit-test a service seam#

Mission (25 minutes): write a test for an order service that reads a repository and sends a notification only after a valid order. Likely files: service test, repository mock, and notification port.

Acceptance checks: the valid case returns a value and verifies one notification; the absent case throws or returns a clear result and sends none; no Spring context starts.

  1. Stub only the repository response needed by the scenario.
  2. Exercise the service through its public method.
  3. Verify the notification only in the branch where it is a business rule.
Solution shape, not a script

Keep fixtures readable: arrange one input and one collaborator response, act once, then assert the output and the consequential interaction.

Transfer: replace the mock repository with an in-memory fake and decide which test now better communicates the rule.

Prepare for interviews: choose the right double#

Two-minute summary: doubles are tools for control and observation; the choice depends on whether the test needs a value, a lightweight implementation, or proof of a collaboration.

Explain in 60 seconds: when would you prefer a fake over a mock?

Primary question: distinguish mock, stub, spy, fake, and dummy. Follow-ups: what makes a Mockito test brittle, and why should you not mock the class under test?

Debug exercise: a stubbed repository returns null unexpectedly. Compare the stubbed arguments with the invocation and propose a safer matcher only where it is meaningful.

Strong-answer outline

Define each double through its role, give one small example, and tie verification to an externally meaningful side effect rather than interaction counting.

Timed mock: describe a fast test strategy for a service with a database and email dependency in 90 seconds.

Exit check#

  1. Which double provides a canned answer?
  2. When is verification more useful than an output assertion?
  3. Why should a test not mock its subject?
  4. What commonly causes an unstubbed null?
  5. What makes a fake different from a stub?

Learn next: Use distinguish test doubles as the base, then write a service test in Proving it works: Mockito & slice tests. Interview next: Use distinguish test doubles as the base, then separate authentication from authorization in Spring Security — Basic Auth.

Continue this path · Switch paths from this topic · Review first: constructor injection

// the big idea

A unit test should exercise your code, not the network, the database, or someone else's API. When a class leans on an external dependency, you swap that dependency for a stand-in you control — a test double. Fakes, stubs, and mocks are three flavors of stand-in, and Mockito is the framework that lets you conjure the last two in a single line instead of hand-writing a class.

§ 0

Notebook Summary#

Copy this by hand. Everything here is derived from the detailed notes below.

Why unit test

  • Myth: tests cost time → Truth: tests speed up development (catch bugs early, refactor safely)
  • Problem: unit = one class in isolation, but real classes call slow / remote / flaky dependencies (Uber API, DB)
  • Fix → replace the real dependency with a test double you control

The 3 test doubles

  • Fake → real working impl, but lightweight & local (in-mem DB instead of prod DB)
  • Stub → canned answers only; returns pre-set response when called. No logic.
  • Mock → stub + records the calls; you can verify() how/when/with-what it was called
  • doodle: 3 boxes in a row labeled Fake / Stub / Mock, arrow of increasing power →

Ladder of pain (why we climb)

  • Real API → can't hit prod / rare corner cases → build a Fake
  • Fake → expensive to build & maintain, format changes → use a Stub
  • Stub → still write a class per test → use a Mock (Mockito) = one line
  • doodle: staircase, 3 steps rising: Fake → Stub → Mock

Mockito core API

  • mock(Class.class) → create the double
  • when(m.method(args)).thenReturn(x) → stub the answer
  • verify(m).method(args) → assert it was called
  • ⭐ when/thenReturn = stubbing  ·  verify = the "mock" part

Annotation style (what you actually write)

  • @ExtendWith(MockitoExtension.class) → enable Mockito on class
  • @Mock → = mock(Foo.class) for that field
  • @InjectMocks → builds class-under-test + injects all @Mocks (via setter/ctor)
  • doodle: @InjectMocks box in center, two @Mock arrows feeding into it

Args & dynamic returns

  • Matchers: any() · anyDouble() · eq(x) → which calls the stub answers
  • Rule: matcher for one arg → matchers for all args (wrap fixed in eq())
  • doAnswer(a).when(m).f(...) → return computed from real args
  •   via invocation.getArgument(0) inside Answer<T>; use for void / arg-dependent

Pitfalls to box

  • UnnecessaryStubbingException → stub set but never called → remove it
  • new inside method overwrites injected mock → real API called (let @Autowired own it)
  • dead stub? widen args to any(), then tighten → step that breaks = bad arg
  • read the exception — Mockito errors are informative

Reading a mock test (template)

  • Code under test → test case
  • Dependency fn(s) → what you mock (can be >1)
  • Mock setup → fn + input + pre-set output (per dependency)
  • Test input → expected output → assert (length and content) + verify

Markers

  • ⭐ TRICK mock the dependency, never the class under test
  • ⭐ TRICK breakpoint doesn't stop in dep method = mock is live
  • ⚠ PITFALL a stubbed when() that's never hit = UnnecessaryStubbing
  • ⚠ PITFALL don't mix eq() + raw value in same call
  • 💡 TIP "stub = state, mock = behavior" → the interview one-liner
DoubleHas real logic?Verifies calls?
Fakeyes (lightweight)no
Stubno (canned)no
Mockno (canned)yes

§ 1

Detailed Study Notes#

Google Maps shows Uber price estimates. To get them, it calls Uber's live pricing API and hands the numbers to users. One afternoon Uber's service starts returning null and negative prices for a handful of routes, and Google Maps crashes for those users. The team patches the code to handle the bad values — but now comes the hard part. How do you test that fix when the buggy responses are rare, live, and owned by a company that has no interest in helping you reproduce them?

You cannot call the real Uber API in a test: it is slow, it costs money, it needs the network, and you cannot force it to return the exact broken data you are guarding against. This single problem — a class you want to test depends on something you do not control — is what the entire idea of test doubles exists to solve.

1.1Why unit test at all#

A unit test checks one small piece of code — usually a single class or method — in isolation from everything else. The common objection is that writing them is a tax on an already-packed schedule, and that solid code does not need them. In practice the opposite holds: tests increase development speed. They catch regressions the moment you introduce them, and they let you refactor aggressively because a green test suite tells you nothing broke.

Analogy · recall

A unit test is a smoke detector wired to one room. It does not tell you the whole house is safe — it tells you, instantly and precisely, that this room just caught fire. The cost is a battery check now and then; the payoff is that you find the fire while it is small.

The word "unit" is the whole tension. A unit is one class. But almost no useful class lives alone — it calls a database, a payment gateway, another team's service. If your test of GMapService secretly reaches across the internet to Uber, you are no longer unit testing; you are testing Uber, the network, and your code all at once, and any of the three can make the test fail.

GMapService the class under test calls ExternalUberService getPriceEstimates() Uber API slow · remote flaky · $$$
fig 1.1 — the dependency you can't control is the whole problem
💡 TIP

The clean seam that makes all of this possible is that GMapService depends on an interface (UberRESTHandler), not a concrete class. In production you inject the real implementation; in a test you inject a stand-in. This is dependency injection doing the heavy lifting — test doubles are only easy because the design left a seam to swap at.

1.2Fakes — a real thing, just smaller#

A fake is a genuine working implementation of the dependency, but a lightweight one that lives inside your test instead of out in the world. It really does the job — it just does it locally and simply. The classic example is swapping a production database for an in-memory collection: same behavior (store, fetch), none of the network, disk, or setup cost.

Analogy · recall

A fake is a stunt double. A real, capable person who actually performs the action — jumps, runs, falls — standing in for the star when using the star is too expensive or dangerous. It genuinely does the work; it is just not the one you ship.

For the Uber problem, a fake means building a small fakeuberapi.com that returns hand-crafted responses, including the null and negative prices you want to test against. It works, and it lets you rehearse corner cases on demand. But two problems surface immediately.

using a Fake — you point at a stand-in service
@Test
public void testGetPriceEstimates() {
    double startLat = 37.775231, startLng = -122.418075;
    double endLat   = 37.775241, endLng   = -122.518075;

    // A Fake: a real handler pointed at a lightweight fake endpoint
    UberRESTHandler externalUberService = new RealUberRESTHandler("https://fakeuberapi.com");

    GMapService gMapService = new GMapService(externalUberService);

    PriceEstimate[] actual = gMapService.getPriceEstimates(startLat, startLng, endLat, endLng);

    assertEquals(5, actual.length);
}
⚠ PITFALL

A fake is real code, so it is real maintenance. When Uber changes its response format, you now have two implementations to update, and a subtle bug in your fake can make a broken test pass or a correct one fail. Worse: can you even faithfully fake a system as large as Uber's? The fake is only as trustworthy as your ability to reproduce the original.

1.3Stubs — canned answers, no logic#

A stub is an object that returns pre-configured answers when its methods are called, and does nothing else. There is no working implementation behind it — no in-memory database, no parsing, no logic. When the code under test asks the stub for price estimates, the stub simply hands back a canned list you decided on in advance (often read from a JSON file in the test's resources folder).

Analogy · recall

A stub is a cardboard cutout of a shopkeeper with a fixed sign around its neck: "Today's price: $12." Ask it anything and it shows you the sign. It cannot compute, negotiate, or react — but for a photo shoot where you only need the sign visible, that is exactly enough.

Stubs shine in the scenario fakes struggle with. Suppose Uber announces a new response format that ships in a month. You implement your side against the proposed format now — but there is no live service returning it yet, and editing your elaborate fake to match is costly. A stub sidesteps all of that: you just declare "when asked, return a response in the new shape," scoped to this one test.

GMapService component under test UberRESTHandler interface — the swap point RealUberRESTHandler injected in production StubUberRESTHandler injected in unit tests
fig 1.3 — same interface, two implementations: real for prod, stub for the test
using a Stub — a class that returns canned data
@Test
public void testGetPriceEstimates() {
    double startLat = 37.775231, startLng = -122.418075;
    double endLat   = 37.775241, endLng   = -122.518075;

    // A Stub: no real logic, just pre-configured responses
    UberRESTHandler externalUberService = new StubUberRESTHandler();

    GMapService gMapService = new GMapService(externalUberService);

    // When getPriceEstimates() calls the stub, it returns the
    // canned response (e.g. read from a JSON file in resources).
    PriceEstimate[] actual = gMapService.getPriceEstimates(startLat, startLng, endLat, endLng);

    assertEquals(5, actual.length);
}

A stub is lighter than a fake, but you still have to hand-write a class — StubUberRESTHandler — and often a new one per test scenario. That is a lot of boilerplate for "just return this list." Which is where mocks, and Mockito, come in.

1.4Mocks — a stub that also watches#

A mock is a more sophisticated stub: it still returns pre-fabricated values, but it also records every call it receives, so you can afterward verify how it was used. Was the dependency called? How many times? In what order? With what arguments? A stub answers "what did my code get back"; a mock also answers "what did my code do."

Analogy · recall

A mock is a cardboard shopkeeper with a hidden clipboard. It still shows the fixed price sign (the stub part), but it quietly writes down every question you asked, when, and how many times. Later you read the clipboard to confirm you actually asked what you were supposed to.

The canonical reason you need this: a method whose whole job is a side effect. Think of code that sends a confirmation email. There is no return value to assert on — success means "the email service was called exactly once with the right address." Only a mock can check that, because only a mock records the call.

💡 TIP · the interview one-liner

Stubs verify state; mocks verify behavior. A stub feeds your code an input and you assert on the output. A mock lets you assert on the interaction itself — that a particular method was called, correctly. If a question hinges on a side effect with no return value, the answer is a mock.

1 · mock() create the double 2 · when() stub the answer 3 · call run code under test 4 · verify() assert the call assertEquals checks the return · verify checks the interaction
fig 1.4 — the mocking flow: create → stub → exercise → verify

Writing a mock class by hand would be even more work than a stub — it has to record calls too. So nobody does. You use a mocking framework: EasyMock, PowerMock, and most commonly Mockito. The framework generates the double, the stubbed answers, and the recording machinery for you, at runtime, in a few lines.

1.5Mockito in three verbs#

Mockito's entire everyday surface is three static methods. Learn these and you can read and write the majority of mock-based tests.

VerbWhat it doesRole
mock(C.class)creates a fake instance of class/interface Cmake the double
when(...).thenReturn(x)"when this method is called, hand back x"the stub part
verify(m).method(...)asserts that method was called on the mockthe mock part
mock() → when · thenReturn → call method under test → assertEquals → verify()
the same test, now with Mockito — no hand-written double
@Test
public void testGetPriceEstimates() {
    double startLat = 37.775231, startLng = -122.418075;
    double endLat   = 37.775241, endLng   = -122.518075;

    List<PriceEstimate> estimateList = someListOfPriceEstimates(); // canned data

    // 1. create the mock
    UberRESTHandler externalUberServiceMock = mock(RealUberRESTHandler.class);

    GMapService gMapService = new GMapService(externalUberServiceMock);

    // 2. stub its response
    when(externalUberServiceMock.fetchEstimates(startLat, startLng, endLat, endLng)).thenReturn(estimateList);

    // 3. exercise the code under test
    PriceEstimate[] actual = gMapService.getPriceEstimates(startLat, startLng, endLat, endLng);

    // check the returned value
    assertEquals(5, actual.length);

    // 4. verify the dependency was actually called
    verify(externalUberServiceMock).fetchEstimates(startLat, startLng, endLat, endLng);
}
⭐ TRICK

Always mock the dependency, never the class you are testing. Here you mock UberRESTHandler (the thing GMapService leans on) and leave GMapService real. If you accidentally mock the class under test, your assertions check the framework's canned answers, not your actual logic — the test turns green while proving nothing.

⚠ PITFALL

A when(...).thenReturn(...) whose arguments never match the real call is silently dead. Mockito returns a default (null, 0, or empty) instead of your canned value, and the test may still pass for the wrong reason. Make the stubbed arguments match what the code under test actually passes — or use argument matchers deliberately.

1.6A template for reading any mock test#

Mock tests get dense fast. A reliable way to decode one — or to plan your own before writing it — is to name six things explicitly. Fill this in for any test and the structure falls out on its own.

Method under test#

gMapService.getPriceEstimates() — the one method whose behavior you are actually checking.

Test case#

In plain words: returns a list of price estimates given valid start/end coordinates.

Dependency function#

externalUberServiceMock.fetchEstimates() — the call you intercept.

Mock setup#

Function to mock + the input to match + the pre-set output to return.

Test input#

The coordinates you pass into the method under test.

Expected output#

The list of price estimates you assert on — plus the verify that the dependency was called.

1.7Fake vs. Stub vs. Mock, side by side#

All three replace a real dependency. They differ in how much they actually do, and in whether they let you inspect how they were used.

 FakeStubMock
Working logic?Yes, lightweightNoNo
Returns canned data?ComputedYesYes
Records / verifies calls?NoNoYes
Effort to buildHighMediumLow (framework)
Answersstatestatestate + behavior
Best whenneed real-ish behavior locallyneed a fixed responseneed to check an interaction / side effect

Reach for a stub#

You care only about what your code gets back. Feed a known input, assert on the output. No interest in whether or how the dependency was called.

Reach for a mock#

You care about the interaction. The dependency's job is a side effect (send email, write log, publish event) with nothing to assert on but the call itself.

1.8The annotation style — how Mockito is really written#

The mock(Class.class) factory call is the honest, explicit way to see what is happening — but in a real Spring project you almost never write it. Mockito ships three annotations that do the same work declaratively, wired up once at the top of the test class. This is the form the take-home exercise actually uses, and the form an interviewer expects you to recognize.

AnnotationWhat it does
@ExtendWith(MockitoExtension.class)registers the test class with Mockito (JUnit 5) so the annotations below are processed
@Mockcreates a mock of that field — the declarative equivalent of mock(Foo.class)
@InjectMocksbuilds the class under test and injects every matching @Mock into it automatically
Analogy · recall

@InjectMocks is a stagehand who dresses the actor before the scene. You declare the props (the @Mock fields) and the actor (@InjectMocks field); the stagehand quietly hands each prop to the actor through the right door (the setters or constructor) before every take, so you never wire them up by hand in each test.

the real Crio test shape — annotations, not factory calls
@ExtendWith(MockitoExtension.class)
class ExternalUberServiceTest {

    @Mock
    private GoogleMapsHelper googleMapsHelperMock;

    @Mock
    private RestTemplate restTemplateMock;

    // built by Mockito; the two mocks above are injected into it
    @InjectMocks
    private ExternalUberService externalUberService;

    @Test
    void getPriceEstimatesValidLocationReturnsPriceEstimatesTest() {
        when(googleMapsHelperMock.isValidLocations(anyDouble(), anyDouble(), anyDouble(), anyDouble())).thenReturn(true);
        // ... exercise externalUberService.getPriceEstimates(...) and assert
    }
}

Before @InjectMocks, every test had to build the object and hand it each mock through setters — new ExternalUberService(), then setGoogleMapsHelper(mock), then setRestTemplate(mock). The annotation collapses all of that into one field declaration and does the injection before every test, through those same setters.

⭐ TRICK · how the injection actually happens

Set a breakpoint inside the setter methods and run in debug: the debugger stops inside setGoogleMapsHelper() and setRestTemplate() even though your test never calls them. That is @InjectMocks pushing the mocks in through the setters for you. Seeing it stop there is the proof that injection is real, not magic.

1.9Argument matchers & returns that depend on the input#

So far every stub returned the same value no matter what. Two refinements from the exercise make stubs far more precise: argument matchers loosen or tighten which calls a stub responds to, and doAnswer lets the return value be computed from the actual arguments.

Argument matchers#

Instead of hard-coding isValidLocations(37.77, ...) you can match by shape: any() matches any object, anyDouble() any double, and eq(x) pins one argument to an exact value while the others stay loose. This is also the go-to debugging move when a stub silently never fires (see §1.10).

matchers — from loosest to most specific
// matches ANY four arguments
when(googleMapsHelperMock.isValidLocations(any(), any(), any(), any()))
    .thenReturn(false);

// matches any four doubles (type-aware)
when(googleMapsHelperMock.isValidLocations(anyDouble(), anyDouble(), anyDouble(), anyDouble())).thenReturn(false);

// pin the first arg exactly, leave the rest loose
when(googleMapsHelperMock.isValidLocations(eq(37.775231), anyDouble(), anyDouble(), anyDouble())).thenReturn(true);
⚠ PITFALL · don't mix raw values and matchers

If you use a matcher for one argument, you must use matchers for all of them. Writing isValidLocations(eq(37.77), someLng, ...) — a matcher plus a raw value — throws an InvalidUseOfMatchersException. Wrap the fixed one in eq(...) so every argument is a matcher.

doAnswer — compute the return from the arguments#

doAnswer() lets a mock decide its answer at call time, based on the exact arguments it received. Where thenReturn(x) is a fixed sign, doAnswer is a tiny function: it hands you an InvocationOnMock, you pull the arguments off it with getArgument(i), and you return whatever those arguments imply. In the exercise this validates a latitude — return true only when it falls within −90 to +90.

doAnswer — return value derived from the real argument
Answer<Boolean> validity = new Answer<Boolean>() {
    public Boolean answer(InvocationOnMock invocation) throws Throwable {
        Double startLatitude = invocation.getArgument(0); // first arg
        return Math.abs(startLatitude) <= 90;             // valid latitude range
    }
};

// use validity when the mock's method is invoked with these arg shapes
doAnswer(validity).when(googleMapsHelperMock)
    .isValidLocations(eq(37.775231), anyDouble(), anyDouble(), anyDouble());
💡 TIP · why doAnswer, not when().thenReturn()

Answer<T> is generic, which is why the return type Boolean is written everywhere. Reach for doAnswer when the response must vary with the input, when the mocked method returns void, or when you want a side effect on invocation. For a flat, input-independent value, plain when().thenReturn() is simpler — don't over-reach for doAnswer.

1.10Common Mockito pitfalls & how to debug them#

Mockito is fiddly to get right, but its error messages are unusually informative — the fastest debugging habit is to actually read the exception rather than re-scan your code. Three failures come up constantly in this exercise.

⚠ PITFALL · UnnecessaryStubbingException

Mockito fails the test if a when().thenReturn() you declared is never actually invoked. This is deliberate: an unused stub means either the method you expected to be called wasn't (a real bug), or the stub was never needed. Fix it by removing the stub the method under test doesn't use — e.g. stubbing isValidLocations() for a test of buildPriceEstimateBaseUrl(), which never calls it.

⚠ PITFALL · re-initializing a field kills the mock

A mock injected into a field is lost the moment the production code reassigns that field with new. In the exercise, adding restTemplate = new RestTemplate() inside the method overwrote the injected mock, the real Uber API got called, and the test failed with an authorization error. Let Spring's @Autowired own field creation; never new a dependency inside the method you're testing.

⭐ TRICK · widen the matcher to find a dead stub

When a stub silently never fires and the test misbehaves, don't stare at the code — loosen the arguments and watch when it starts working. Swap your specific values for any(), confirm the stub now gets hit, then tighten one argument at a time (anyDouble(), then eq(value)). The step where it breaks is the argument that didn't match the real call.

💡 TIP · confirm a mock is really a mock

Doubtful whether the injected object is the mock or the real thing? Put a breakpoint in the dependency's method — with a mock in place, the debugger will not stop there, because the mock replaces the real implementation entirely. Or hover the field in the debugger: its type shows as a Mockito-generated proxy, not the plain class.

1.11The Mock Design template#

The exercise gives one reusable planning template — fill it in before writing a mock test and the code writes itself. It expands the six-part reading frame from §1.6 to handle multiple mocked dependencies, which real tests usually have.

Mock Design template — fill this in before you code
Code Under Test:
Test case:

Dependency Layer/Function:
  1.
  2.

Mock Setup:
  - Function to mock:
    - Input to mock:
    - Pre-set output from mock:
  - Function to mock:
    - Input to mock:
    - Pre-set output from mock:

Test Input:
Expected Test Output:

Worked for the exercise's own test, getPriceEstimatesValidLocationReturnsPriceEstimatesTest(), it fills in like this:

the template, completed
Code Under Test:  ExternalUberService.getPriceEstimates()
Test case:        valid coordinates return a populated PriceEstimate[]

Dependency Layer/Function:
  1. GoogleMapsHelper.isValidLocations()  -> validates the coordinates
  2. RestTemplate.getForObject()          -> fetches the price-estimate response

Mock Setup:
  - Function to mock: googleMapsHelperMock.isValidLocations
    - Input to mock: the start/end lat & lng coordinates
    - Pre-set output from mock: true
  - Function to mock: restTemplateMock.getForObject
    - Input to mock: the built request URL
    - Pre-set output from mock: JSON read from price_estimate.json

Test Input:          valid start/end coordinates
Expected Test Output: PriceEstimate[] of length 3 with the expected fare strings
💡 TIP · assert on content, not just length

A stronger test checks the actual values, not only the array size — e.g. assertEquals(3, actual.length) followed by assertEquals("₹110-140", actual[0].getEstimate()). Length alone passes even when the parsing is subtly wrong.


§ 2

Interview & Practice Q&A#

The follow-up questions this material naturally invites. Answers are short enough to say out loud.

What is a test double, and why do we need them?

A test double is any stand-in object you inject in place of a real dependency during a test. You need them because a unit test should exercise one class in isolation, but real classes call things you cannot use in a test — slow, remote, costly, or flaky dependencies like a live API or production database. The double lets you control that dependency completely.

What is the difference between a fake, a stub, and a mock?

A fake is a real but lightweight working implementation, like an in-memory database standing in for a production one. A stub has no logic; it just returns pre-configured canned responses. A mock is a stub that also records the calls it receives, so you can verify afterward that a method was called the right number of times, in the right order, with the right arguments.

Stubs and mocks both return canned data — so what actually separates them?

Verification. A stub only feeds data back to your code; you assert on the resulting state. A mock additionally records interactions, so you can assert on behavior — that the dependency was actually invoked correctly. The one-liner: stubs verify state, mocks verify behavior.

When would a mock be the only option that works?

When the method under test's important effect is a side effect with no return value — sending an email, publishing an event, writing to a log. There is nothing to assert on except "the collaborator was called correctly," and only a mock records that call.

Walk through the four steps of a Mockito test.

Create the mock with mock(Dependency.class). Stub its response with when(mock.method(args)).thenReturn(value). Exercise the real class under test, which internally calls the mock. Finally, assert on the return value and use verify(mock).method(args) to confirm the dependency was called as expected.

Why do we mock the dependency instead of the class under test?

Because the class under test is the thing whose real logic you want to validate. If you mock it, you are only checking the framework's canned responses, not your code — the test passes while proving nothing. You mock the collaborators around it so the class itself runs for real against controlled inputs.

What goes wrong with building a fake for a large external API?

Two things. It is expensive to build and maintain — every time the real API changes format, you must update your fake too. And it may not be genuinely reproducible: faithfully faking a system as large and complex as Uber's is hard, so your fake can drift from reality and give you false confidence.

A Mockito test passes but you suspect it proves nothing. What do you check?

Check that the stubbed when(...) arguments actually match what the code under test passes. If they don't match, Mockito returns a default (null/0/empty) instead of your value, so the test can pass for the wrong reason. Also confirm you mocked the dependency, not the class under test, and that a meaningful verify or assert is present.

What does dependency injection have to do with test doubles?

It is the seam that makes them possible. Because the class under test depends on an interface and receives its implementation from outside (via the constructor), you can inject the real one in production and a double in tests without changing the class. Without that seam, you would have no clean way to swap in a stand-in.

What do @Mock, @InjectMocks, and @ExtendWith(MockitoExtension.class) each do?

@ExtendWith(MockitoExtension.class) registers the test class so Mockito processes its annotations. @Mock creates a mock for that field — the declarative form of mock(Foo.class). @InjectMocks builds the class under test and injects every matching @Mock into it automatically, through its constructor or setters, before each test.

When would you use doAnswer() instead of when().thenReturn()?

When the return value has to depend on the actual arguments the mock was called with, when the mocked method returns void, or when you need a side effect at call time. doAnswer gives you the InvocationOnMock, from which getArgument(i) reads each argument so you can compute the response. For a fixed, input-independent value, when().thenReturn() is simpler.

What is an UnnecessaryStubbingException and how do you fix it?

Mockito throws it when a when().thenReturn() you set up is never actually called during the test. It is a safety net: an unused stub usually means the method you expected to be invoked wasn't (a real bug) or the stub was never needed. Fix it by removing the stub the method under test doesn't use, or by correcting why the expected call never happened.

Why can re-initializing a field with `new` inside a method break a mock-based test?

Because the injected mock lives in that field, and reassigning it with new replaces the mock with a real object mid-test. In the exercise, doing restTemplate = new RestTemplate() inside the method caused the real Uber API to be called, which failed with an authorization error. Let Spring's @Autowired own field creation and never new a dependency inside the code under test.


§ 3

Key Takeaways#

  • A unit test isolates one class; real dependencies (APIs, DBs) must be replaced with a controllable stand-in — a test double.
  • Fake = real but lightweight implementation. Stub = canned responses, no logic. Mock = stub that also records calls for verification.
  • The progression fake → stub → mock is a ladder of decreasing effort and increasing power; Mockito collapses the last step into a few lines.
  • Mockito's core: mock() to create, when().thenReturn() to stub, verify() to check the interaction.
  • In practice you write it with annotations: @ExtendWith(MockitoExtension.class), @Mock for each dependency, @InjectMocks to build and wire the class under test automatically.
  • Argument matchers (any(), anyDouble(), eq()) control which calls a stub answers — all-or-nothing per call. doAnswer() computes the return from the actual arguments.
  • Stubs verify state; mocks verify behavior. Use a mock when the thing that matters is a side effect with no return value.
  • Watch for UnnecessaryStubbingException (unused stub), new overwriting an injected mock, and matcher/raw-value mixing. Read the exception — Mockito's errors are informative.
test double unit = one class fake = real, small stub = canned mock = stub + verify mock() when · thenReturn verify() @Mock @InjectMocks @ExtendWith any() · eq() doAnswer() state vs behavior mock the dependency UnnecessaryStubbing

§ 4

Microexercises — Build the Muscle#

Reading a mock and writing one are different muscles. Do each of these before you open the walkthrough — sketch the test in an editor or on paper first. Struggling for a minute is what turns "I recognise verify()" into "I can reach for it without thinking."

Exercise 1 · Stub a slow API, verify the call#

// your task

You're unit-testing WeatherReporter. It depends on WeatherApi, a slow external service you must not hit in a unit test. Write a JUnit 5 + Mockito test proving that report("Dubai") returns "Dubai: hot" when the API reports 41 °C — and that the API was asked exactly once, for Dubai.

  • Replace WeatherApi with a test double.
  • Assert on the returned string (verify state).
  • Confirm the collaborator was called correctly (verify behavior).
given — the classes under test
public interface WeatherApi {
    double tempCelsius(String city);   // the real impl hits the network
}

public class WeatherReporter {
    private final WeatherApi api;
    public WeatherReporter(WeatherApi api) { this.api = api; }

    public String report(String city) {
        double t = api.tempCelsius(city);
        return city + ": " + (t >= 30 ? "hot" : "mild");
    }
}
Show the full walkthrough

Map the problem onto the four-step template — create → stub → exercise → verify — and each line writes itself.

Step 1 — create the double. mock(WeatherApi.class) hands you a stand-in whose every method returns the type's default (0.0 for a double) until you say otherwise. No network, no real implementation.

Step 2 — stub the response. when(api.tempCelsius("Dubai")).thenReturn(41.0). The argument "Dubai" is load-bearing: this stub fires only when the method is called with exactly "Dubai". Any other city falls through to the default.

Step 3 — exercise the real code. Build the real WeatherReporter (never a mock of it) with the double injected, then call the method you're testing. Its real logic — the >= 30 threshold and the string formatting — runs for real.

Step 4 — assert & verify. assertEquals checks the returned value (state); verify(api).tempCelsius("Dubai") checks that the collaborator was actually invoked once, with the right argument (behavior).

WeatherReporterTest.java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;

class WeatherReporterTest {

    @Test
    void reportsHotWhenApiSaysHot() {
        // 1 · create the double
        WeatherApi api = mock(WeatherApi.class);

        // 2 · stub — the argument must match the real call
        when(api.tempCelsius("Dubai")).thenReturn(41.0);

        // 3 · exercise the REAL class under test
        WeatherReporter reporter = new WeatherReporter(api);
        String result = reporter.report("Dubai");

        // 4 · assert state + verify behavior
        assertEquals("Dubai: hot", result);
        verify(api).tempCelsius("Dubai");
        verifyNoMoreInteractions(api);   // optional: nothing else was touched
    }
}

Why it's a fair test: only the untestable network hop is replaced; your real branching logic still executes. And because WeatherReporter takes its dependency through the constructor, swapping in the double needs no change to the class — that seam is dependency injection earning its keep.

⚠ if the argument doesn't match

Stub when(api.tempCelsius("dubai")) (lowercase) instead, and the real call tempCelsius("Dubai") never matches it. Mockito returns the default 0.0, report returns "Dubai: mild", and the test fails for a reason that has nothing to do with your logic. A stub only fires on an exact argument match.

Exercise 2 · The test that passes-but-proves-nothing (diagnose & fix)#

// your task

This test compiles, and its author swears it "covers the happy path." Read it, predict what happens when you run it, then fix it so it actually proves the behavior.

the suspect test
public class Greeter {
    private final UserRepo repo;
    public Greeter(UserRepo repo) { this.repo = repo; }
    public String greet(long id) {
        return "Hi, " + repo.findById(id).name();
    }
}

@Test
void greetsUser() {
    UserRepo repo = mock(UserRepo.class);
    when(repo.findById(1L)).thenReturn(new User("Trinity"));

    Greeter greeter = new Greeter(repo);
    String msg = greeter.greet(2L);          // ← asks for id 2

    assertEquals("Hi, Trinity", msg);
}
Show the full walkthrough

Predict first. greet(2L) calls repo.findById(2L). But the stub only answers for 1L. For any un-stubbed argument a Mockito mock returns the type's default — and for an object return type, that default is null.

So repo.findById(2L) returns null, and null.name() throws a NullPointerException. The test never reaches the assertion — it errors out. It doesn't "pass"; it crashes, which is Mockito's way of shouting that the stub never fired.

Root cause: the stubbed argument (1L) doesn't match the exercised argument (2L). This is the number-one silent-mock trap — a stub is keyed to an exact argument.

the fix — make them match
@Test
void greetsUser() {
    UserRepo repo = mock(UserRepo.class);
    when(repo.findById(1L)).thenReturn(new User("Trinity"));

    Greeter greeter = new Greeter(repo);
    String msg = greeter.greet(1L);          // ← now matches the stub

    assertEquals("Hi, Trinity", msg);
    verify(repo).findById(1L);
}

The deeper lesson. Had findById returned a String instead of an object, the mismatch would have been quieter still: you'd get "Hi, null" and a failed assertion rather than a crash — at least that points at the value. The NPE here is the loud version of the same bug. And note the other classic form: mocking UserRepo is correct — you mock the dependency. Write Greeter greeter = mock(Greeter.class) and you'd be testing Mockito's canned response, not your code.

create → stub → exercise → verify mock(Dependency.class) when · thenReturn verify() argument must match unstubbed → default (null/0) mock the dependency state vs behavior