---
title: 'Unit Tests'
source: 'https://academia.sh/en/courses/architecture-testing-and-release/unit-tests'
course: 'Architecture, Testing and Release'
language: en
updated: '2026-10-21T05:03:08+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes the separate roles of the test target, the test class and the assertion in a unit test'
    - 'Explains by measurement that a check passing does not show the code is correct'
    - 'Compares an assertion that tries the boundary value with one that tries a value far from the threshold'
    - 'Shows that when the expected value is copied from the faulty code, the assertion rejects the correct version'
---

# Unit Tests

Setting up a unit test with a test target, an assertion and a test double, and measuring in four categories whether the same boundary check tells the correct version from the faulty one.

In the previous lesson (Modularization) we looked at how a dependency that crosses a module's boundary becomes visible. This lesson moves to another kind of boundary: the one where a value changes the rule. If free shipping on a cart starts at a hundred units, the easiest place for the code to break is exactly the value one hundred. A unit test exists to try these places.

The previous course (Data, Networking and Concurrency) compared two runs of an operation: does the result change if the operation is repeated or its order changes. This course asks something different. Here we look at a single **check** and apply it to two versions: the correct version of the code and a version into which we deliberately put a fault. If the check passes on the correct version and fails on the faulty one, it is doing its job. If it passes on both, it is telling us nothing.

The measurement axis is **separation**. Four cases come out, and they appear in the output under their ASCII names. A **separating** check passes on the correct version and fails on the faulty one. A **blind** check passes on both; it cannot see the fault. A **noisy** check fails on both; it rejects even the correct code. An **inverted** check fails on the correct version and passes on the faulty one; that is, it rewards the fault.

| Category | Correct version | Faulty version |
|---|---|---|
| separating | passes | fails |
| blind | passes | passes |
| noisy | fails | fails |
| inverted | fails | passes |

The core below classifies these four cases. We do not assign the label ourselves; `separation` is derived only from the two truth values.

```swift
// Separation.swift — classifies whether a check tells the correct version from the faulty one
struct Contrast<Output: CustomStringConvertible> {
    let label: String
    let correct: Output
    let faulty: Output
    let passesCorrect: Bool
    let passesFaulty: Bool

    var separation: String {
        switch (passesCorrect, passesFaulty) {
        case (true, false): return "separating"
        case (true, true): return "blind"
        case (false, false): return "noisy"
        case (false, true): return "inverted"
        }
    }

    func describe() -> String {
        let onCorrect = passesCorrect ? "pass" : "fail"
        let onFaulty = passesFaulty ? "pass" : "fail"
        print("\(label) | correct=\(correct)/\(onCorrect) faulty=\(faulty)/\(onFaulty) -> \(separation)")
        return separation
    }
}

func contrast<Output>(_ label: String, correct: Output, faulty: Output, check: (Output) -> Bool) -> Contrast<Output> {
    Contrast(label: label, correct: correct, faulty: faulty, passesCorrect: check(correct), passesFaulty: check(faulty))
}

func countSeparations(_ results: [String]) {
    for name in ["separating", "blind", "noisy", "inverted"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}
```

`Contrast` holds the output of the two versions and the check's verdict on those outputs. The `contrast` function applies the same `check` closure first to the correct output, then to the faulty one. `describe` writes the verdict on a single line. The reason for the rule: running a check on one version asks it nothing. A check is worth what it can tell apart between the two versions.

## Test Target, Test Class and Assertion

A unit test has three parts. The **test target** is the package compiled separately from the app's code; the tests live there and import the app code. The **test class** is an `XCTestCase` subclass; each method in it whose name starts with `test` is a single experiment. An **assertion** is the call that compares the result of the experiment with the expected value; if it does not hold, the test fails.

The example below checks the shipping fee of a cart. The rule is this: if the subtotal is one hundred or more, shipping is free; below that it costs fifteen units.

```swift
// shipping fee test — SOURCE
import XCTest

final class ShippingFeeTests: XCTestCase {
    func testFeeAtThreshold() {
        XCTAssertEqual(shippingFee(for: 100), 0)
    }

    func testFeeFarAboveThreshold() {
        XCTAssertEqual(shippingFee(for: 500), 0)
    }

    func testFeeBelowThreshold() {
        XCTAssertEqual(shippingFee(for: 40), 15)
    }
}
```

This fragment is not run; the test framework runs inside the test runner of the development environment. `XCTAssertEqual(_:_:)` compares two values. Each method runs independently; even if one method fails, the others still run. To prepare values, `setUp()` is called before each method and `tearDown()` after.

The same three experiments can also be written in a second form. In this form a function marked with `@Test` is used in place of a method, and the `#expect(_:)` macro in place of an assertion.

```swift
// shipping fee test, second form — SOURCE
import Testing

@Test func feeAtThreshold() {
    #expect(shippingFee(for: 100) == 0)
}

@Test func feeFarAboveThreshold() {
    #expect(shippingFee(for: 500) == 0)
}
```

When a `#expect` assertion does not hold, it does not stop the test; the following assertions still run. If it needs to stop, `#require(_:)` is used. In the older form the counterpart of `#expect` is the `XCTAssertTrue(_:)` call, and the counterpart of `#require` is the `XCTUnwrap(_:)` call. Both forms ask the same question; in this lesson we measure only that shared question.

## The Shipping Fee Model

The **model** below sets up what the check says to the two versions; it is not the test framework itself. In the model, `shippingFee(for:)` exists in two versions. The correct version returns zero at the threshold and above. In the faulty version the comparison is written as `>` instead of `>=`, so a subtotal of exactly one hundred is still charged. This is the best-known fault of unit tests: one side of a boundary is counted wrong.

```swift
// Fees.swift — correct and faulty versions of the shipping fee, with a check helper
func correctFee(for subtotal: Int) -> Int {
    subtotal >= 100 ? 0 : 15
}

func faultyFee(for subtotal: Int) -> Int {
    subtotal > 100 ? 0 : 15
}

func feeCheck(_ label: String, subtotal: Int, expected: Int) -> String {
    contrast(label, correct: correctFee(for: subtotal), faulty: faultyFee(for: subtotal), check: { $0 == expected }).describe()
}
```

`feeCheck` is the model of one of the assertions above: it asks both versions for the fee at the same subtotal and, in place of `XCTAssertEqual(…, expected)`, performs the comparison `$0 == expected`. The labels are the ASCII kebab-case names of the tests. Next to the three real experiments we added a fourth: a test whose expected value is written wrong. This way three categories appear together.

```swift
// main.swift — four checks, two versions
let atThreshold = feeCheck("threshold-fee", subtotal: 100, expected: 0)
let farAbove = feeCheck("far-above-fee", subtotal: 500, expected: 0)
let below = feeCheck("below-fee", subtotal: 40, expected: 15)
let wrongSpec = feeCheck("wrong-expected", subtotal: 40, expected: 10)

countSeparations([atThreshold, farAbove, below, wrongSpec])
```

```
threshold-fee | correct=0/pass faulty=15/fail -> separating
far-above-fee | correct=0/pass faulty=0/pass -> blind
below-fee | correct=15/pass faulty=15/pass -> blind
wrong-expected | correct=15/fail faulty=15/fail -> noisy
separating: 1
blind: 2
noisy: 1
inverted: 0
```

`threshold-fee` is separating: the correct version returns zero and the check passes; the faulty version returns fifteen and the check fails. `far-above-fee` and `below-fee` are blind: both pass on both versions, because the two versions behave the same far from the value one hundred. `wrong-expected` is noisy: since the expected value is ten, even the correct version fails.

What stands out here is that all three tests could look green. Of the three real checks, two passed on the faulty version too. A test suite that does not try the boundary value can report code carrying the fault as green. This is the first sign that coverage and the number of tests alone give no guarantee; we will measure coverage separately in the third lesson.

**TP1.** This measurement tried a single fault, `>` written in place of `>=`. If the code had broken in another way, for example if the threshold were ninety-nine instead of one hundred, which check separates would change.

**TP2.** We know the model's `faultyFee` function because we put the fault there ourselves. In real code the location of the fault is not known in advance; this is why the separation measure is meaningful together with mutation testing (deliberately breaking the code and watching whether the test reacts), where the fault is known.

## Why the Boundary Value Separates

The difference between a separating check and a blind check can be read as a rule: **try the value where the rule changes**. If the threshold is one hundred, the boundary values are ninety-nine, one hundred and one hundred one. One hundred carries the most information because it separates the two sides of the rule. A difference between `>=` and `>` appears only at this value; no other input can tell the two versions apart.

Blind checks are not worthless. `below-fee` says the fee is fifteen, and that is a behavior contract; if someone changes the fee later, the test catches it. But as a check meant to find the fault, it is blind. The two roles are separate: a test can pin down behavior and still be unable to separate a particular fault. The separation measure looks at the second.

The question to ask while writing a test is this: if I applied this check to the faulty version, would it fail. If the answer is yes, the check separates. If no, you write it knowing it is blind. The cheapest way to answer the question is to break the comparison in the code for a minute after writing the test and see whether the test turns red.

## Test Doubles

Real cart code does not compute the threshold and the fee itself; it gets them from a pricing service, a clock or a network client. A unit test does not run these dependencies in their real form, because a test must be fast and deterministic. **Dependency injection** is giving the dependency from outside; the test also puts a **test double** in its place.

A test double has three common forms. A **stub** returns a ready-made answer. A **spy** records that it was called, and the call is inspected afterwards. A **dummy** only fills a place and is never called. These three are collectively also called a **mock**.

```swift
// test double — SOURCE
protocol RateProviding {
    var freeShippingThreshold: Int { get }
}

struct StubRates: RateProviding {
    let freeShippingThreshold = 100
}

final class SpyReceipt {
    private(set) var printedTotals: [Int] = []
    func print(total: Int) { printedTotals.append(total) }
}
```

`StubRates` is a stub: it gives the threshold as a fixed value, and the test is sure the threshold is one hundred. `SpyReceipt` is a spy: it keeps which totals were written to the receipt, and the test later looks at the `printedTotals` array. This fragment also stays SOURCE; it is only type declarations and has no output to measure.

The limit of a test double is this: when a stub gives a fixed value, the behavior of the code with the real dependency is not measured. Because the threshold is pinned at one hundred, a fault where the threshold arrives wrong from the server does not come out of this test. A unit test measures the logic of the code; another test layer measures the components working together.

## The Bounding Measurement

The sneakiest fault of a unit test is in the expected value itself. When a test is written, the expected value comes from a source called a **test oracle**: a specification, a hand calculation or another independent path. Sometimes there is no oracle. The author runs the code, sees the value that comes out and pastes it into the assertion. The value passes at that moment, because the assertion is the code's own output.

The measurement below does this. The same `threshold-fee` assertion that tries the boundary value is taken, but the expected value is copied from the output of the faulty version.

```swift
// main.swift — expected value copied from the faulty code
let pasted = faultyFee(for: 100)
let copied = feeCheck("pasted-expected", subtotal: 100, expected: pasted)

countSeparations([copied])
```

```
pasted-expected | correct=0/fail faulty=15/pass -> inverted
separating: 0
blind: 0
noisy: 0
inverted: 1
```

The same assertion that tries the boundary value was separating in the previous measurement. Now it is **inverted**: the correct version fails, the faulty version passes. The check itself did not change; what changed is where the expected value comes from. When the output of the faulty code is made the oracle, the test approves the fault and later, when the code is fixed, turns red. The team blames the test, not the fault.

This is why separation depends not only on what the check does but also on the source of the expected value. A good oracle exists before the code is read: the cart specification, a hand-calculated example or the documented output of the previous version.

**TP3.** This measurement makes the copy from the faulty version. A value copied from the correct version would have made the assertion separating; but even then the value can be trusted only if its correctness is known beforehand.

**TP4.** The measurement checks a single boundary, one hundred. All three boundary values would have given the same inverted result, but we did not measure that separately.

## Common Mistakes

- Trying only values far from the threshold. Such checks stay blind.
- Copying the expected value by running the code. If the oracle is the code itself, the test approves the fault.
- Checking more than one behavior in a single test. When the test fails, it is unclear which behavior broke.
- Making a test complicated by trying to make a test double resemble the real thing. A stub should behave only as much as the test needs.
- Accepting a test as green without ever seeing it red. A test must have failed at least once on code that was deliberately broken.

## Summary

- A unit test lives in the test target; each method in the test class is a single experiment, and an assertion compares the result with the expected value.
- A check passing does not show the code is correct; separation is measured by whether the check tells the correct version from the faulty one.
- Of four checks, one was separating, two blind and one noisy; the check that tried the boundary value was the only separating one.
- When the expected value was copied from the faulty code, the same check became inverted: it rejected the correct version and accepted the faulty one.
- Test doubles keep a test fast and deterministic but do not measure behavior with the real dependency.
- This lesson's separation: **separating**.

## Next Step

A unit test checks the logic of the code; it does not check the screen the user sees. In the next lesson, UI Tests will measure which checks of a driver that taps a button find the fault, and which break only when the text changes.
