# Foundation 5. Testability

## The test as a consumer

A functional test represents a consumer of the module. Enter through its API and check results, status or messages that belong to the contract.

The black box does not have to cover the entire system. It can be a small module as long as the test respects its limit and does not turn internal coordination into a public promise.

```text
test → API → implementation
                  └── API consumed → contributor
```

The main question is what should remain true for the consumer. The choice between real implementation, test double or integration comes later.

## What a test can observe

A contract offers three types of observation:

| Observation | Generic example |
|---|---|
| Direct result | Returned value or error |
| Status accessible by API | A subsequent query reflects the change |
| Outgoing message | An event is published or an effect is requested |

The helpers used, the distribution between internal objects and the chosen algorithm are left out. Also left out are the number and order of calls when they do not change the observable result.

A functional test should accept two implementations that produce the same results, the same visible state, and the same contractual messages.

## Example: preserve a value on failure

Let's consider a cache module:

```text
Cache.Refresh(Key) → Value | SourceUnavailable
Cache.Get(Key) → Value | NotFound
```

`Cache` consumes another API:

```text
Source.Load(Key) → Value | mistake
```

We want to demonstrate a single guarantee: if the source fails during an update, the old value remains available.

The test runs the actual implementation of `Cache` and prepares a double of `Source` that returns a failure. Then just look at the API:

```text
given a value stored for a key
when Refresh receives SourceUnavailable
then returns SourceUnavailable
and Get retains the previous value
```

The test does not need to know whether the cache writes a copy first, uses a lock, or rolls back an allocation. You also don't need a real remote source, because the observed risk is the module's reaction to the failure, not the network protocol.

A different test should check the actual adapter if the risk was in serialization, configuration, or transport.

## Example in Go

Assuming the above API, the case can be written without observing any internal calls.

```go
import (
    "errors"
    "testing"
)

type sourceStub struct{ err error }

func (s sourceStub) Load(string) (string, error) {
    return "", s.err
}

func TestRefreshKeepsPreviousValue(t *testing.T) {
    cache := NewCache(sourceStub{err: ErrSourceUnavailable})
    cache.Put("key", "previous")

    _, err := cache.Refresh("key")
    if !errors.Is(err, ErrSourceUnavailable) {
        t.Fatalf("expected source error, got %v", err)
    }

    got, _ := cache.Get("key")
    if got != "previous" {
        t.Fatalf("expected previous value, got %q", got)
    }
}
```

The test controls an indirect input through a stub and checks result and status. It does not dictate how many helpers the cache should run.

## Speak with usual vocabulary

**Test double** is the general term for any substitution used during a test. Within that family it is advisable to maintain the known meanings, instead of redefining `fake` to encompass everything.

| Term | Usage |
|---|---|
| Actual implementation | The same implementation used outside the test |
| Fake | Simplified functional implementation, such as an in-memory store |
| Stub | Returns prepared responses to handle indirect input |
| Spy | Record messages so you can see them later |
| Mock | State and verify expectations about interactions |

The terminology comes from the taxonomy collected by Gerard Meszaros and summarized by Martin Fowler in [*Test Double*](https://martinfowler.com/bliki/TestDouble.html). Knowing the name helps, but the important decision remains what behavior the double replaces and what risk it leaves uncovered.

In the previous example, a stub of `Source` is enough: it prepares the ruling that the case needs. A functional fake would be useful if many tests needed an in-memory source with stable rules. A mock would only make sense if a specific interaction was part of the contract.

## Choose collaborators

Real implementation offers the highest fidelity and is the first choice when it is fast, deterministic, airtight, secure, and easy to build. [*Software Engineering at Google*'s guide on test doubles](https://abseil.io/resources/swe-book/html/ch13.html) proposes the same pragmatic starting point.

| Situation | Regular choice |
|---|---|
| Fast and deterministic collaborator | Actual implementation |
| Many cases need stable semantics without infrastructure | Functional Fake |
| A case needs an exceptional response | local stub |
| The contract includes an outgoing message | Spy or mock |
| Risk depends on protocol, transaction or configuration | Integration with real adapter |

It is not necessary to substitute values, entities or pure functions just because they help the case. Nor is it necessary to build real infrastructure to demonstrate a rule that does not depend on it.

## When to observe interactions

An outgoing message may form part of the contract. In that case, a spy or a mock allows you to observe its content, quantity or order.

The expectation should be limited to the functional difference:

- content, if another consumer depends on those fields;
- quantity, if duplicating or omitting the message changes the external effect;
- order, if the protocol requires it.

Checking that a mapper, a specific query, or a private helper was called freezes the implementation. Verifying that a required message was posted protects the contract.

An interaction only proves that the message was attempted to be sent. It does not demonstrate that the actual recipient accepts it. When compatibility is the risk, adapter testing or integration is required.

## What should a fake keep

A functional fake implements a declared subset of the contract. Has to:

- accept the same inputs in supported cases;
- produce compatible results, errors and status;
- be deterministic and isolate the state of each test;
- visibly reject capabilities that it does not implement;
- document the properties you omit.

You don't need to copy the infrastructure. An in-memory source can reproduce read, miss, and versions without simulating network or latency. If the case deals precisely with those omitted properties, the fake is no longer sufficient evidence.

A shared fake accumulates responsibility and deserves its own tests. When feasible, the same contract suite can be run against the fake and the real adapter to detect divergences.

## Review method

To design a test:

1. Name the module, the API consumed and the guarantee that you want to protect.
2. Choose a public observation: result, status or message.
3. Remove claims about internal coordination.
4. Run real collaborators while they are practical.
5. Enter the smallest double that allows you to control or observe the case.
6. State what fidelity is missing and what risk needs integration.
7. Check that another correct implementation would also pass.

> **First decide what should be observed; then choose which implementations it can be demonstrated with.**
