Course outline

iOS / Swift / Concurrency and data boundaries / Lesson 12

Swift Testing, XCTest, and test boundaries

Use deterministic model checks and separate them from Apple-hosted UI, persistence, and device evidence.

About 5 min readContent reviewed
On this page

Lesson goals

  • Select the smallest boundary that can expose the defect
  • Assert behavior rather than repeat the implementation
  • Control asynchronous completions rather than sleeping

Select the smallest boundary that can expose the defect

A pure parser test establishes an input rule. A model test with a fake repository establishes transitions and completion ordering. A UI automation test exercises controls through an application host. These forms of evidence complement one another but are not interchangeable. Define the failure you want to detect before selecting a framework. Swift Testing supports expressive tests and concurrency integration in a compatible toolchain; XCTest remains relevant for established suites and UI testing. Check the project’s actual toolchain and target availability instead of assuming a test macro compiles on every Swift installation.

Assert behavior rather than repeat the implementation

The first fragment tests a deliberately small positive-count parser. Its expectations are concrete domain outcomes, not a second algorithm that mirrors each branch. Include distinct boundaries such as absence, malformed text, zero, and an accepted value when those contracts matter. A failing expectation should identify which behavior was violated. The snippet requires a compatible Swift Testing host and is not a standalone app. No tests were run during authoring, so the described pass remains an expectation. Keep test fixtures synthetic and avoid copying real customer records merely to make examples realistic.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
import Testing
func acceptedCount(_ text: String) -> Int? {
    guard let value = Int(text), value > 0 else { return nil }
    return value
}
@Test func positiveCountContract() {
    #expect(acceptedCount("4") == 4)
    #expect(acceptedCount("0") == nil)
    #expect(acceptedCount("four") == nil)
}
Expected output
Expected: the three positive-count expectations pass in a compatible test host.

The parser accepts only the shown integer contract. No Swift Testing macro expansion or execution was performed here.

Control asynchronous completions rather than sleeping

For stale results, give a fake separate pending requests that the test resolves in a chosen order. Start A and B, complete B, then A, and inspect the accepted result. For cancellation, pause at a known checkpoint and cancel before continuing. An arbitrary millisecond delay can pass accidentally without exercising the race. Keep continuation ownership explicit and ensure every pending continuation is resumed exactly once on every test path. A fake that leaks tasks or never resolves after a failed assertion can make the suite hang and conceal the original defect.

Use XCTest where the suite boundary calls for it

The second fragment demonstrates an XCTest assertion for a value-model operation from the basic level. It creates a fresh model inside the test, preventing shared mutation across concurrently executed tests. Tests should not rely on a global draft changed by the previous method. Framework-specific assertion APIs differ, but isolation and outcome quality matter more than mechanically converting syntax. For UI tests, drive public accessible controls and inspect meaningful results; calling a private save method is a model test, not evidence that a user can reach or understand the action.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
import XCTest
final class WorkshopValueTests: XCTestCase {
    func testReservationPreservesOriginalSnapshot() throws {
        let original = try Workshop(id: "w-7", seats: 5)
        var draft = original
        try draft.reserve(2)
        XCTAssertEqual(original.seats, 5)
        XCTAssertEqual(draft.seats, 3)
    }
}
Expected output
Expected: the original remains 5 and the independently edited draft becomes 3.

Requires the Workshop value model from the basic lesson and an XCTest target. No simulator or compiler execution is claimed.

Separate fakes from integration guarantees

An in-memory repository fake can verify how the application responds to failure, but cannot establish URLSession transport, SwiftData disk durability, Keychain behavior, permission prompts, or signing. An in-memory persistence test exercises more than a pure fake yet still differs from restart recovery on a device. State the implementation actually used and the boundary crossed. Use target integration tests for the contracts that depend on Apple runtime or system configuration, in an authorized isolated environment. Do not expand a local pass into a claim that every supported device and accessibility setting is certified.

Report reproducibility and unexecuted limits

Record compiler version, language mode, SDK, test target, dependency lock state, and the minimal fixture for meaningful failures. For screenshot checks, also control device configuration, locale, text size, and fonts. Review visual changes before updating an expected image. Keep a concise validation statement identifying executed checks and outstanding target boundaries. In this course, schema and bilingual content checks are performed while Swift Testing, XCTest, UI automation, and Apple runtime examples remain unexecuted. Predicted results support teaching but must never be copied into a project report as recorded evidence.

Common errors and fixes

A timing test passes only occasionally

Why it happens
Wall-clock sleeps replaced explicit control of operation completion.
How to fix it
Use deterministic fake responses or scoped continuation checkpoints.
Prevent it next time
Test the problematic ordering directly.

Tests affect one another through shared state

Why it happens
A mutable global fixture outlives individual tests.
How to fix it
Create isolated models and dependencies for each case.
Prevent it next time
Run with the framework’s documented parallelism in mind.

A fake persistence pass is called durable storage validation

Why it happens
The tested implementation did not exercise the claimed boundary.
How to fix it
Report fake policy, in-memory context, and device persistence evidence separately.
Prevent it next time
Name the actual host in validation reports.

Check your understanding

1. What does the XCTest snapshot test establish about a real server reservation?

Show worked solution

Nothing about server acceptance or concurrent capacity. It verifies the local value model’s copy and mutation contract. Add separate service and target integration evidence for remote authorization, conflict handling, and uncertain completion. Do not dilute the useful local result by claiming more than it tests.

2. Should every helper function receive a test that mirrors its implementation?

Show worked solution

No. Focus on meaningful externally observable contracts and failure risks. A test that reproduces the same calculation can preserve the same bug. Choose boundary examples, invariants, and interaction outcomes that would distinguish correct behavior from a plausible incorrect implementation.

Key takeaways

  • Select the smallest boundary that can expose the defect
  • Report reproducibility and unexecuted limits

Official references