---
title: Mocking
source: 'https://academia.sh/en/courses/testing-performance-and-release/mocking'
course: 'Testing, Performance and Release'
language: en
updated: '2026-10-21T05:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Tells test double, mock, stub, spy, and dummy apart by their roles'
    - 'Compares the call count of a spy with the budget and classifies the overrun'
    - 'Explains that a stub brings network calls down to zero and that this does not measure real network behavior'
    - 'Shows that call verification is an assertion that pins the count'
---

# Mocking

Mocking puts a test double in place of a real dependency in a test. The lesson counts how many times a repository goes to the network source in each operation and shows what a test double does and does not let you measure.

The functions measured in the previous lesson touched no outside resource: an input came in and a step count came out. A real repository, however, goes to a network source, and a unit test does not want that trip. A test that reaches the network gets slow, depends on the state of the network, and can give two different results for the same input. The fix is to replace the dependency with its test double. In this lesson the course's question stays the same: **in how many samples does this measurement exceed its budget?** The four answers are the same too: within, minority, majority, all. The unit of work measured in this lesson is the number of calls that go to a network source.

The core is the same as in the previous lesson. `Budget` carries the name and the limit of a budget, `Overrun` takes the label, the budget, and the list of samples of a measurement; it counts the samples that strictly exceed the limit and picks the overrun.

```kotlin
// Overrun.kt — classifies in how many samples a measurement exceeds its budget
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "within"
        over == total -> "all"
        over * 2 < total -> "minority"
        else -> "majority"
    }

    fun line() {
        println("$label: budget=${budget.name}<=${budget.limit} | over=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("within", "minority", "majority", "all")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

## The Four Kinds of Test Double

Every place where the class under test connects to the outside is called a dependency. The object that takes the place of this dependency in a test is called a test double. A test double has four kinds by role, and each goes by its own name.

A dummy only fills a parameter; it is never called and gives no answer. A stub gives a predetermined answer: whatever it is asked, it returns its ready answer. A spy records and counts calls; it can keep the real behavior or give a ready answer. A mock knows the expected calls in advance and breaks the test if an expected call does not arrive or if too many arrive. In this lesson's model we will build the spy and the stub; we will see the mock in the platform piece.

Because the concepts overlap, drawing a line helps: the stub decides the answer, the spy counts the call. The stub carries the answer to "what was answered", the spy carries the answer to "how many times was it asked". The number measured in this lesson is the second: how many times a repository goes to the network source.

## Two Calls, One Budget

Think of an order repository. When it reads an order, the repository goes to the network source twice, first for a freshness check and then to fetch the actual data. Whether this behavior is a bug is a product decision; the model takes the behavior as it is and counts it. The network source is an interface. In production a class that goes to the network implements it; in a test a spy that counts calls takes its place.

```kotlin
// Source.kt — network source contract, a spy that counts calls, and a cache that gives ready answers as a stub
interface RemoteSource {
    fun fetch(key: String): String
}

class SpySource(private val replies: Map<String, String>) : RemoteSource {
    var requestCount: Int = 0
        private set

    override fun fetch(key: String): String {
        requestCount++
        return replies[key] ?: "none"
    }
}

class CacheStub(private val entries: Map<String, String>) {
    fun lookup(key: String): String? = entries[key]
}
```

`SpySource` increases the `requestCount` counter by one on every `fetch` call; the counter is read from outside but written only inside the class. `CacheStub` only returns its ready answers and counts no call. Now the repository.

```kotlin
// OrderRepository.kt — calls the network source twice per operation: one is a freshness check
class OrderRepository(
    private val source: RemoteSource,
    private val cache: CacheStub = CacheStub(emptyMap()),
) {
    fun load(key: String): String {
        val cached = cache.lookup(key)
        if (cached != null) return cached
        source.fetch(key)
        return source.fetch(key)
    }
}
```

The repository looks at the cache first. If the cache gives an answer, it does not go to the network; if it does not, it goes twice. The default cache is empty, that is, it never gives an answer. The measuring function also goes into a helper file: for each operation it builds a separate spy, runs the repository, and returns the call count seen in that operation.

```kotlin
// Probe.kt — measures the number of calls that go to the network source in each operation
fun callsPerOperation(keys: List<String>, stub: CacheStub = CacheStub(emptyMap())): List<Int> =
    keys.map { key ->
        val spy = SpySource(mapOf(key to "network"))
        OrderRepository(spy, stub).load(key)
        spy.requestCount
    }
```

This function produces a list of numbers from a list of inputs; its return value is what will be given to `Overrun` as the samples. Eight orders will be read and the budget is one call per operation. This budget is the model's counterpart of a product decision that wants the screen to go to the network once when it opens an order; neither the platform nor a network provider sets such a limit.

**TE1.** The model does not run the mocking library; it is not present in this environment. The spy and the stub are hand-written classes, not objects produced by the library. The budget of 1 call per operation is an input of the model.

```kotlin
// Main.kt
fun main() {
    val keys = listOf(
        "order-1", "order-2", "order-3", "order-4",
        "order-5", "order-6", "order-7", "order-8",
    )
    val calls = callsPerOperation(keys)
    println("call counts: $calls")

    val overrun = Overrun("repo-calls", Budget("call", 1), calls)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
call counts: [2, 2, 2, 2, 2, 2, 2, 2]
repo-calls: budget=call<=1 | over=8/8 -> all
within: 0
minority: 0
majority: 0
all: 1
```

All eight operations made two calls, and each exceeded the budget by one call. The measurement is all. All says that the function is outside the budget: changing the input does not change the result, because the source of the overrun is not a property of an input but the repository's own design. The rule does not see magnitude, and here the difference is one call; the prose says that this one call equals one network trip.

## Call Verification

A unit test can pin the same number. The spy counts the call, and the test verifies that the counter has the expected value. With a library that builds mocks, it is written like this.

```kotlin
// Platform code: OrderRepositoryTest
class OrderRepositoryTest {
    @Test
    fun loadAsksSourceOnce() {
        val source = mock<RemoteSource>()
        whenever(source.fetch("order-1")).thenReturn("ok")

        OrderRepository(source).load("order-1")

        verify(source, times(1)).fetch("order-1")
    }
}
```

The piece does two jobs. The `whenever` call builds a stub: whenever `fetch("order-1")` is asked, it returns `"ok"`. The `verify` call is a call verification: the source must have been asked exactly once. Because the repository asks the source twice, this test breaks. The mock the library produces is the platform counterpart of the model's `SpySource` and `requestCount`, but it is not the same thing. The model sets the rule of this measurement; the model is not the tool itself. The break of the test is an overrun: the expected count is one, the actual count is two.

There is a difference when comparing the rule with an assertion. An assertion breaks or does not break for a single operation; the rule says in how many of eight operations it broke. The assertion gives an error, the rule gives prevalence. Someone who sees a failure in a test and wants to measure it runs the same test on eight inputs and reads the overrun.

## What the Library's Lines Do

Each of the five lines in the platform piece does a separate job, and knowing which line corresponds to which kind of test double is the way to understand what the test checks.

`mock<RemoteSource>()` builds a fake implementation of the interface, generated at run time. In this object no method does real work; every call returns without a reply and is recorded. In the model, the hand-written `SpySource` takes its place, but unlike it, this object carries a log rather than a counter.

The line `whenever(source.fetch("order-1")).thenReturn("ok")` builds the stub. The first half states which call is meant, and the second half states which answer will be given to that call. Once the stub is built, it returns the same answer however many times the same call arrives.

`OrderRepository(source).load("order-1")` is the class under test itself. The mock is given to the constructor as the dependency; the repository does not know it is fake. Giving the dependency from outside, that is, injecting it through the constructor, is a required condition for mocking. If the repository built the source inside itself, the test would have no place to step in.

`verify(source, times(1)).fetch("order-1")` is the verification line. The library looks at the log and checks whether the call `fetch("order-1")` was made exactly once. If another number were written instead of `times(1)`, the expectation would change too. The failure of this line is the direct result of the two calls in the repository.

## The Limit of the Mock

Call verification is a powerful tool, and for exactly that reason it needs care. When a test pins in which order and how many times the repository calls the source, it has pinned the implementation, not the behavior. If the repository one day drops the freshness check and lowers the call count to one, the behavior has not changed, but a test that expects two calls breaks. The test team takes this break for a bug and has to repair the test.

The reason we set the budget at one in this lesson rests on a change in the opposite direction: a product decision wants one call, and the test pins that decision. The difference lies in who the decision comes from. If a budget rests on a product decision, its overrun is a finding; if it merely copies the current implementation, its overrun is noise. The measurement cannot tell the two apart; the team does.

## Which Dependency Is Doubled With Which Kind

The question to ask when choosing the kind of test double is what the test expects from that dependency. If the test needs only an answer, a stub is enough: let the cache return a value and let the repository pass that value on. If the test wants to check whether a call was made, a spy or a mock is needed: was the network reached, how many times. If the dependency exists only to fill the constructor's signature, a dummy is enough and no behavior is written.

If the choice is made wrongly, the test either becomes needlessly complicated or falls short. A test that wants to check the call count cannot see the count if it uses a stub; a test that looks only for an answer breaks on the verification of unrelated calls if it builds a mock. In this lesson we used two kinds: the network source was doubled with a spy to count the call, and the cache was doubled with a stub to give an answer. Each dependency was doubled with the kind that fits its role.

## Common Misconceptions

The first misconception is the thought that "every dependency should be doubled". Mocking cuts the dependency off from real behavior. If the dependency is fast, deterministic, and free of side effects, such as a pure calculation function, doubling it only lengthens the test and keeps it from checking the real behavior.

The second misconception is the thought that "a mock is faster than the real object, so it is always better". A mock is fast, but what it checks is the behavior of the mock. We saw this in the measurement where the stub gave an old answer: the call count was zero, the answer was wrong.

The third misconception is the thought that "the call count is the measure of performance". The call count is the number of network trips; not every trip lasts the same. Two calls can exceed a budget and the total duration can still stay short. In this lesson the number is used as a unit of work, not as a duration.

## The Bounding Measurement

The bounding measurement here is the cache with a stub. The reasoning is this: the first measurement coming out all showed that the overrun came not from the input but from the design. The way to confirm this is to change the design and measure the same input again. The input, the budget, and the number of samples stay fixed; only the cache changes. That way, the difference in the result cannot be put down to another factor.

Now read the same eight operations with a stub. The stub cache gives a ready answer for every key. When the repository finds an answer in the cache, it does not go to the network.

```kotlin
// Main.kt — with a stub cache the network calls drop to zero
fun main() {
    val keys = listOf(
        "order-1", "order-2", "order-3", "order-4",
        "order-5", "order-6", "order-7", "order-8",
    )
    val stub = CacheStub(keys.associateWith { "cache" })
    val calls = callsPerOperation(keys, stub)
    println("call counts: $calls")

    val overrun = Overrun("stubbed-repo", Budget("call", 1), calls)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
call counts: [0, 0, 0, 0, 0, 0, 0, 0]
stubbed-repo: budget=call<=1 | over=0/8 -> within
within: 1
minority: 0
majority: 0
all: 0
```

The same repository, the same eight keys, the same budget. The network calls dropped to zero in all eight, and the measurement moved from all to within. The behavior of the repository did not change here: the repository still goes twice if the cache gives no answer. What changed is that the test never enters that path of the repository.

This is the limit of the mocking measurement. The stub decides the answer, that is, the test measures the answer of the stub, not of the real network. Zero calls do not say that the network is fast or that the repository works well; they say only that the stub answered every question. A measurement shows this: put an old value in the stub, knowing that the network gives a new value.

```kotlin
// Main.kt — the difference between the stub and the network is what the rule does not see
fun main() {
    val keys = listOf("order-1", "order-2", "order-3", "order-4")
    val stale = CacheStub(keys.associateWith { "stale" })
    val fresh = keys.associateWith { "fresh" }

    val fromStub = keys.map { OrderRepository(SpySource(fresh), stale).load(it) }
    val fromNetwork = keys.map { OrderRepository(SpySource(fresh)).load(it) }
    println("answers from the stub: $fromStub")
    println("network answers: $fromNetwork")

    val calls = callsPerOperation(keys, stale)
    println("call counts: $calls")
    val overrun = Overrun("stubbed-stale", Budget("call", 1), calls)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
answers from the stub: [stale, stale, stale, stale]
network answers: [fresh, fresh, fresh, fresh]
call counts: [0, 0, 0, 0]
stubbed-stale: budget=call<=1 | over=0/4 -> within
within: 1
minority: 0
majority: 0
all: 0
```

The stub returned `stale` in all four operations; the network was saying `fresh` for the same keys. The call count is again zero, the measurement again within. The rule counts the call and does not look at whether the answer is correct. This is not a bug but the purpose of the rule; yet a test written with a stub being green does not mean the real network will give the same answer. What closes that gap is another kind of test: the instrumented tests, closer to the real dependency, that we will see in the next lesson.

**TE2.** The model builds the stub as a map; a real mocking library can also have a stub give different answers by call order, which the model does not have.

## Summary

- This lesson's overrun: all. The repository went to the network source twice in every operation, and eight of eight operations exceeded the budget of one call.
- The lesson's criterion, the number of calls that go to the network source in one operation: the spy counts, the stub decides the answer, the dummy only fills a parameter, the mock verifies the expected call.
- With the stubbed cache, the calls in the same eight operations dropped to zero and the measurement moved from all to within; the repository did not change, the path the test enters did.
- A stub has its own answer measured: while the network said the new value, the stub returned the old one and the call count was zero again.
- Call verification breaks or does not break for one operation; the rule says in how many operations the overrun occurred.

## Next Step

In this lesson, the objects that took the place of the network source were hand-written classes, and none of them ran on a device. Instrumented tests turn this around: instead of a test double they use a real screen and a real system, but each test builds that screen from scratch. The next lesson, On-Device Tests, measures in how many tests this setup exceeds the budget.
