Unit Testing with Swift Testing
iOS Development — Architecture & Data
Chapter 6 · Unit Testing with Swift Testing
Chapter 5's own FakeAPIClient was built specifically to make this chapter possible. Apple's
real Swift Testing framework, introduced with Xcode 16 (September 2024), is the tool
that actually runs and verifies real tests against it.
Swift Testing vs. the Older XCTest
Swift Testing is a genuinely newer, Swift-native alternative to Apple's older XCTest framework — real, lighter-weight syntax built around Swift macros rather than XCTest's own class/method-naming conventions.
| XCTest (Older) | Swift Testing (Real, Current — Xcode 16+) | |
|---|---|---|
| Marking a Test | A method starting with test | @Test on any real function |
| Asserting | XCTAssertEqual(a, b) | #expect(a == b) |
| Fatal Check | XCTUnwrap(optional) | try #require(optional) |
| Async Tests | Real, but more setup-heavy | Real, native async function support |
@Test and #expect
TaskListViewModel(apiClient: FakeAPIClient()) is only possible because Chapter 5 refactored
the ViewModel to accept its dependency via a real, injected protocol. Without that refactor, this test
would either need a real, live network connection, or simply couldn't be written at all.
A failed #expect is real but non-fatal — the test method keeps running,
and every other #expect in the same test still executes, reporting its own real pass or fail
independently.
Testing Real Async Code
async is all that's needed — Swift Testing genuinely awaits
it directly, with no extra expectation objects or completion handlers the way older async testing
approaches often required.
#require: A Real, Fatal Check
#expect reports a failure and keeps going. #require is real and
fatal — if the condition fails, the test stops immediately, and it doubles as a genuine,
real way to safely unwrap an optional for the rest of the test:
#require Here#require, accessing viewModel.tasks.first! directly would force-unwrap
(Fundamentals Chapter 2's own real risk) — a genuine crash, not a clean, reported test failure, if the
array happened to be empty. #require turns that same risk into a real, clean, informative
test failure instead.
Hands-On Exercises
Write a real @Test function togglingATaskFlipsItsDoneState() that creates a TaskListViewModel with FakeAPIClient(), adds a task, calls toggleDone(for:) on it, and uses #expect to confirm isDone is now true.
Write a real @Test async function that calls refresh() on a TaskListViewModel, then uses try #require to safely unwrap the first task before checking its title with #expect.
Explain, in your own words, why #expect being non-fatal and #require being fatal are each the genuinely correct default for different real situations, using one concrete example of each from this chapter.
Chapter 6 Quick Reference
- Swift Testing (Xcode 16, September 2024) — a real, current, Swift-native alternative to XCTest
@Testmarks any real function as a test — no naming convention required, unlike XCTest'stest-prefixed methods#expect(...)— non-fatal; a failure is reported, but the test keeps runningtry #require(...)— fatal; stops the test immediately, and safely unwraps an optional for the rest of it- Real, native
asynctest function support — no extra expectation objects needed - Chapter 5's own dependency injection is what makes real, network-free ViewModel testing possible at all