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 TestA method starting with test@Test on any real function
AssertingXCTAssertEqual(a, b)#expect(a == b)
Fatal CheckXCTUnwrap(optional)try #require(optional)
Async TestsReal, but more setup-heavyReal, native async function support

@Test and #expect

import Testing @Test func addingATaskIncreasesTheCount() { let viewModel = TaskListViewModel(apiClient: FakeAPIClient()) viewModel.add(title: "Buy milk", priority: 1) #expect(viewModel.tasks.count == 1) #expect(viewModel.tasks.first?.title == "Buy milk") }
The Real, Direct Payoff of Chapter 5
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

@Test func refreshLoadsTasksFromTheFakeClient() async { let viewModel = TaskListViewModel(apiClient: FakeAPIClient()) await viewModel.refresh() #expect(!viewModel.tasks.isEmpty) }
Real, Native Async Support
Marking the test function itself 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:

@Test func theFirstTaskHasTheExpectedTitle() throws { let viewModel = TaskListViewModel(apiClient: FakeAPIClient()) viewModel.add(title: "Buy milk", priority: 1) let firstTask = try #require(viewModel.tasks.first) // real, fatal if tasks is empty #expect(firstTask.title == "Buy milk") // safe: firstTask is a real, non-optional Task here }
A Real, Genuine Reason to Prefer #require Here
Without #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

Exercise 1

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.

📄 View solution
Exercise 2

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.

📄 View solution
Exercise 3

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.

📄 View solution

Chapter 6 Quick Reference

  • Swift Testing (Xcode 16, September 2024) — a real, current, Swift-native alternative to XCTest
  • @Test marks any real function as a test — no naming convention required, unlike XCTest's test-prefixed methods
  • #expect(...) — non-fatal; a failure is reported, but the test keeps running
  • try #require(...) — fatal; stops the test immediately, and safely unwraps an optional for the rest of it
  • Real, native async test function support — no extra expectation objects needed
  • Chapter 5's own dependency injection is what makes real, network-free ViewModel testing possible at all