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
First characterize what’s hard: hidden singletons, entanglement between layers or lack of separation of concerns. Pick the smallest seam that lets you assert a meaningful outcome.
Prefer smaller targeted improvements over big-bang rewrites. The goal is one place that returns a result your tests can observe, not a spider-web of side effects.
Introduce protocols only where you need substitution; avoid abstract factories everywhere. A thin protocol around URLSession, NSCache, or analytics is usually enough.
Principles
- Inject dependencies at construction not via globals.
- Separate pure logic from UI; even a private function extracted to a
struct/enumcan become unit-testable. - Prefer value types for inputs/outputs.
- Make side effects explicit — if a method “does work,” return a result or expose a callback you can capture in tests.
Use a small protocol when the production dependency talks to the outside world. The test can pass a fake implementation 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")
}
Pure functions are ideal unit-test targets because the same input always produces the same output and there are no side effects 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")
}