Question 2
Designing for testability
You inherit a feature that's hard to test. How do you refactor or redesign it to make it testable without over-engineering?
Answer outline
Start by naming what makes it hard: hidden singletons, layers that reach into each other, or one method that does five things. Then find the smallest seam, the one spot where you can swap a dependency or observe a result, and assert one meaningful outcome there.
Prefer small targeted changes over a big-bang rewrite. Each step should leave you with one place that returns a result a test can observe, instead of a web of side effects.
Introduce a protocol only where you need substitution. A thin protocol around URLSession, NSCache, or your analytics client is usually enough. Abstract factories on every type are the over-engineering the question warns about.
Principles
- Inject dependencies through the initializer instead of reaching for globals.
- Separate pure logic from UI, because a function moved into a plain
structorenumis trivial to unit test. - Prefer value types for inputs and outputs so tests can build and compare them directly.
- Make side effects explicit by returning a result or exposing a callback the test can capture.
A small protocol wraps the dependency that talks to the outside world, so the test passes a stub without touching networking, disk, or global state:
protocol UserFetching {
func fetchUser(id: String) async throws -> User
}
final class ProfileViewModel {
private let fetcher: UserFetching
init(fetcher: UserFetching) {
self.fetcher = fetcher
}
func loadName(id: String) async throws -> String {
let user = try await fetcher.fetchUser(id: id)
return user.name
}
}
struct StubUserFetcher: UserFetching {
func fetchUser(id: String) async throws -> User {
User(id: id, name: "Ada")
}
}
func testLoadNameUsesInjectedFetcher() async throws {
let sut = ProfileViewModel(fetcher: StubUserFetcher())
let name = try await sut.loadName(id: "1")
XCTAssertEqual(name, "Ada")
}
A pure function is the easiest unit test target, because the same input always produces the same output and there's nothing to mock:
enum PriceFormatter {
static func displayPrice(cents: Int, currency: String = "$") -> String {
let dollars = Double(cents) / 100
return "\(currency)\(String(format: "%.2f", dollars))"
}
}
func testDisplayPriceFormatsCents() {
XCTAssertEqual(PriceFormatter.displayPrice(cents: 1299), "$12.99")
XCTAssertEqual(PriceFormatter.displayPrice(cents: 0), "$0.00")
}



