---
title: 'Faking and Interface Separation'
source: 'https://academia.sh/en/courses/go-production/faking-and-interface-separation'
course: 'Production Go: Testing, Tooling and Performance'
language: en
updated: '2026-09-14T10:59:33+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Reads from the source that the Notifier interface binds ReduceStock to a contract, not a concrete implementation'
    - "Runs the same test body with a real and a fake implementation and reads the spy object's call count"
    - 'Limits, with the case of reducing more than the stock, that an assumption holding in the real implementation is never tested in the fake one'
---

# Faking and Interface Separation

The same ReduceStock body is run with both a recording and a spy Notifier implementation: in a normal reduction both give the same verdict, and the spy object's call count reads 1. The limiting measurement shows that in a call reducing more than the stock, the real implementation rejects the negative remainder while the spy object silently records the same call.

The previous lesson measured that an HTTP handler can be tested without ever making a
real connection, but the handler itself still looked at a fixed store built by hand.
This lesson moves that store to another dependency behind the handler — a notification
sender — and asks the same question: if this dependency is something slow, unreliable,
or side-effecting in the real world (writing to an external system, for instance), how
do you test the same logic without touching that dependency at all?

The previous lesson had, in a sense, already solved this question:
`httptest.NewRecorder` was a pre-written fake that implemented the standard library's
own `http.ResponseWriter` interface. This lesson applies the same pattern to a
dependency that has no ready-made fake in the standard library: this package itself
writes `Notifier` and its two implementations.

The fake dependency family — mock object, stub, imitation object, spy object, dummy
object — was built with its taxonomy in the Unit Testing and Test-Driven Development
course and is not reintroduced here; neither is the taxonomy retold nor is every member
implemented separately in this lesson. This lesson's added question is narrow: what
makes faking possible, and where does that thing appear in the source?

In production codebases it is often not possible to test everything a function calls in
its real form: a notification sender might write to a real external system, a payment
provider might move real money, a storage layer might connect to a real server. A fake
standing in for these dependencies frees the test from these costs — but this freedom
has its own price, and this lesson's limiting measurement counts exactly that price. As
long as the price stays invisible it carries no risk; this lesson's goal is to make the
price visible, not to make you abandon faking.

## What Makes Faking Possible Is Interface Separation

One more function is added to the `inventory` package: `ReduceStock` reduces an item's
quantity and sends a notification when the remaining amount drops below a certain
threshold — the threshold is fixed at 2 in this lesson. `ReduceStock` does not call the
notification sender directly — it calls it through a `Notifier` **interface**.

```go
// inventory.go — ReduceStock is bound not to a concrete notification sender but to an interface
package main

import (
	"errors"
	"fmt"
)

type Item struct {
	Name     string
	Quantity int
	Price    int
}

type Notifier interface {
	Notify(name string, remainingQuantity int) error
}

func ReduceStock(store map[string]Item, name string, amount int, n Notifier) error {
	it := store[name]
	it.Quantity -= amount
	store[name] = it
	if it.Quantity < 2 {
		return n.Notify(name, it.Quantity)
	}
	return nil
}

// recordingNotifier is the real implementation: it rejects a negative remainder.
type recordingNotifier struct {
	records []string
}

func (r *recordingNotifier) Notify(name string, remainingQuantity int) error {
	if remainingQuantity < 0 {
		return errors.New("a negative remainder cannot be reported")
	}
	r.records = append(r.records, fmt.Sprintf("%s:%d", name, remainingQuantity))
	return nil
}

// spyNotifier is the fake implementation: it records the call without validating it.
type spyNotifier struct {
	calls int
}

func (s *spyNotifier) Notify(name string, remainingQuantity int) error {
	s.calls++
	return nil
}
```

**TM10.** `ReduceStock`'s last parameter is `Notifier` — an interface consisting of
nothing but a method signature, not a concrete type like `recordingNotifier` or
`spyNotifier`. This is a decision that can be read directly from the source: the code
calling `ReduceStock` can pass in **any** type that carries the `Notify` method, and
`ReduceStock`'s body never knows which concrete type was passed. This is exactly what
makes faking possible: if `ReduceStock` took `*recordingNotifier` directly, there would
be no way at all to pass in something else in its place.

The `inventory` package gained exactly one new dependency throughout this lesson, and
this dependency was defined inside the package itself — `Notifier` is not a type
imported from a real external package, it is `inventory`'s own interface, defining only
the single method `ReduceStock` needs. This is an example of the consumer of an
interface (here, `ReduceStock`) defining a small interface to fit its own needs: an
interface is not the broad contract a library exposes to the outside world, but the
narrow contract that covers only as much as the caller actually uses.

The `Notifier` interface carries a single method, and this narrowness is deliberate: the
narrower an interface is, the less work it takes to write a type that implements it.
Both `recordingNotifier` and `spyNotifier` satisfy the interface by defining only this
one method (`Notify`) — in Go, a type does not have to separately declare that it
satisfies an interface; if its method set covers the set the interface asks for, the
type satisfies that interface on its own. This is why `spyNotifier` can be passed to
`ReduceStock` while having no connection at all to the rest of the `inventory` package,
only by carrying the `Notify` method.

The interface itself guarantees that the real and the fake implementation carry the
**same contract**: both can be called with the same signature
(`Notify(name string, remainingQuantity int) error`), and `ReduceStock`'s body runs the
same line (`n.Notify(name, it.Quantity)`) without distinguishing which one it is working
with. But this contract only guarantees the **signature** — not the **behavior** behind
it. Whether the two implementations give the same answer to the same input cannot be
read from the interface itself at all; the answer to that question emerges in the
measurement below.

## Same Body, Two Implementations

`ReduceStock`'s body has exactly one line concerning the `Notifier` type:
`n.Notify(name, it.Quantity)`. Which concrete type this line works with depends on which
value the caller passed to `ReduceStock` — the function itself never makes this
decision. The measurement below leaves this decision to the caller, calling the same
function twice with two different third arguments.

**TM11.** The same `ReduceStock` call is run first with the real implementation
(`recordingNotifier`), then with the fake implementation (`spyNotifier`); two are
reduced from a three-quantity item, the remainder (1) is below the threshold, so the
notification fires.

```go
// measurement.go — the same body runs with a real and a fake implementation
package main

import "fmt"

func main() {
	fmt.Println("-- run: same body with the real and the fake --")
	store1 := map[string]Item{"pen": {"pen", 3, 250}}
	real := &recordingNotifier{}
	errReal := ReduceStock(store1, "pen", 2, real)
	fmt.Println("real: error present:", errReal != nil, "record count:", len(real.records))

	store2 := map[string]Item{"pen": {"pen", 3, 250}}
	fake := &spyNotifier{}
	errFake := ReduceStock(store2, "pen", 2, fake)
	fmt.Println("fake: error present:", errFake != nil, "spy call count:", fake.calls)
}
```

```
-- run: same body with the real and the fake --
real: error present: false record count: 1
fake: error present: false spy call count: 1
```

The measurement builds two separate `store` variables (`store1`, `store2`), with the
same content but as separate maps — since `ReduceStock` mutates the store itself, if a
single store were used twice, the second call would start from the state the first call
left changed, and the two implementations' verdicts would no longer rest on the same
input. This isolation rests on the same reasoning as `tools.Setup`'s building a
completely empty module on every run in the earlier lessons: one measurement must not
carry the trace of a previous one.

The body of `ReduceStock` is byte-for-byte identical across both runs; the only thing
that changes is the value passed as the last argument. In the real implementation, the
record list's length rises to 1 — this tracks a real side effect. In the fake
implementation, the same call raises the `calls` counter to 1 — all the spy object
measures is this: **how many times** it was called, not **what** it did. Both
measurements arrive at the same result (`error present: false`), and to this extent it
can be said that the fake stands in for the real one.

`spyNotifier` is specifically a spy object here — the member of the fake dependency
family that records calls and allows them to be queried afterward. `Notify` does
nothing when called, it only increments a counter; this is enough to test whether
`ReduceStock` fired a notification, because the measurement's question is "was a
notification sent," not "was the notification's content correct." The same scenario
could be extended with a spy that also keeps the call's arguments instead of just the
count — `spyNotifier` is kept in its narrowest form here, just a counter, because that
is all this measurement asks.

The name `recordingNotifier` is itself a choice too: this type is not just a dummy that
records calls, it is an implementation that really makes a decision — rejecting a
negative remaining quantity is not a fake rule, it is logic that would go into
production. This lesson calls it "real" because it builds a small model of the actual
implementation `ReduceStock` will work with in production — it, too, writes to no
external system in this lesson, but the validation rule it carries is of the kind a real
external system might carry.

## The Limiting Measurement: The Fake Implementation Tests Less

`ReduceStock` itself does not reject a call that would push the quantity negative — this
is a design decision: how reducing more than the stock is handled was left to
`Notifier`'s responsibility, not `ReduceStock`'s. This is a branch invisible in the
previous measurement; when two were reduced from a three-quantity item, the remainder
always stayed positive.

**TM12.** The same measurement is now run reducing five from a three-quantity item —
the remaining quantity drops negative (-2) and `Notify` is called with this negative
value.

```go
// boundary.go — an assumption holding in the real implementation is never tested in the fake one
package main

import "fmt"

func main() {
	fmt.Println("-- limiting measurement: reducing more than the stock --")
	store3 := map[string]Item{"pen": {"pen", 3, 250}}
	real2 := &recordingNotifier{}
	errReal2 := ReduceStock(store3, "pen", 5, real2)
	fmt.Println("real: error present:", errReal2 != nil)

	store4 := map[string]Item{"pen": {"pen", 3, 250}}
	fake2 := &spyNotifier{}
	errFake2 := ReduceStock(store4, "pen", 5, fake2)
	fmt.Println("fake: error present:", errFake2 != nil, "spy call count:", fake2.calls)
}
```

```
-- limiting measurement: reducing more than the stock --
real: error present: true
fake: error present: false spy call count: 1
```

The call count comes out the same in both implementations — `fake2.calls` is still 1,
because `Notify` is still called exactly once. What changes is the **outcome** of the
call, not the call itself; the spy object correctly counts whether the call happened,
but never looks at whether the value the call carried is valid. This time the two
implementations reach **different** verdicts. `recordingNotifier` encodes an assumption
a real external system might carry in the real world: a negative remaining quantity
cannot be reported, because such a state is meaningless in the external system's eyes.
When this assumption is violated, the real implementation returns an error, and
`ReduceStock` itself carries this error along unchanged to its caller. `spyNotifier`
carries no assumption at all — it only records that it was called, and always reports
success; called with the same negative value, it silently raises `calls` by 1 and
returns `nil`.

This is a limit specific to this lesson within the topic's measurement axis: a test
suite using only `spyNotifier` never tests whether `ReduceStock` correctly handles the
case of reducing more than the stock — because the spy object itself does not
distinguish this case, it accepts every call in the same way. An assumption that holds
in the real implementation goes entirely untested in the fake implementation; a faking
test therefore tests **less** than the real test, not more. This is not a flaw but the
nature of a fake: a fake does not carry all the rules of the real system behind it, only
as much as the person who wrote it encoded.

This has a concrete production-side counterpart: even if a test suite written only with
a spy object looks green, `ReduceStock` will hit an error in this scenario once the real
external system engages — and the rule producing that error might be written in the
external system's own documentation, yet never carried into the fake implementation at
all. This is another form of the limit measured in Test Package Basics: there, a passing
test did not say it was correct, only that what it tested held; here, a passing faking
test says only that the fake's **own** rules held — not the real system's rules. Both
limits trace to the same root: a test passing carries no claim about anything outside
the scope of what it tested. The number of methods the fake implements is the same as
the real implementation here (both implement only `Notify`), but the number of
**behaviors** is not: the real implementation carries two behaviors (accepting,
rejecting), the fake carries only one (accepting). The interface itself never shows this
difference, because an interface only enforces the signature, not the number of
behaviors.

This limit does have an exit, though it is outside this lesson's scope: writing a
separate contract test that periodically tests the fake implementation against the real
implementation's assumptions could separately verify that the two sides' behavior does
not drift apart. There is no such contract test in this lesson — the goal is to show the
fake's limit, not to offer a solution that closes it.

`spyNotifier` is the right choice for this measurement, because the question asked is
"how many times was it called" — the other members of the fake dependency family exist
to answer different questions. A stub would return a fixed value and record nothing; a
dummy object would only fill a parameter slot, and it would make no difference if
`Notify` were never called at all. The behavior of `ReduceStock` measured here — whether
the notification is **triggered** and **how many times** it is triggered — is exactly
the kind of information a spy object measures; that is why this lesson used only that
one of the five members.

## Summary

- `ReduceStock`'s last parameter is not a concrete type but an interface (`Notifier`);
  this separation can be read directly from the source, and it is exactly what makes
  faking possible — the information that makes faking possible is stated by the
  **source**.
- The same `ReduceStock` body was run with a real (`recordingNotifier`) and a fake
  (`spyNotifier`) implementation; in a normal reduction both gave the same verdict.
- What the spy object measures is only the call count — not what it did, but how many
  times it was called.
- The limiting measurement: in a call reducing more than the stock, the real
  implementation rejected the negative remainder, the fake implementation silently
  accepted the same call — the faking test never tested this assumption at all.
- The fake dependency taxonomy was built in the Unit Testing and Test-Driven Development
  course; it is not retold here, only measured through Go's own interface machinery.

## Next Step

This lesson measured that an interface makes a dependency swappable between a real and a
fake implementation, but that the fake implementation does not carry all the real one's
assumptions. `Notifier` stayed a narrow, single-method interface; `recordingNotifier`
carried its own validation rule, `spyNotifier` kept only a call counter — and the two
implementations gave the same verdict in a normal reduction and different verdicts in an
excessive one. The four lessons so far each passed or failed one at a time, gave or did
not give an error one at a time — but none of them asked which **number** among these
verdicts changes from run to run. The next lesson asks exactly this question: when two
writings of the same work are compared, which number stays the same across five separate
runs, and which does not — and the topic's binding numbers are also set up there.
