Senior / Staff iOS — interview prep
Testing
pyramid · XCTest · doubles · async · MainActor · CI · flakes · confidence
UnitIntegrationXCUITestStubFakeasync testsMainActorDeterminism
01
What to test where
Pyramidi
Decide by risk and cost of failure — not a rigid ratio. More unit, some integration, very few UI tests.
| Layer | Best for | Watch out |
|---|---|---|
| Unit | Pure logic, parsers, reducers, mappers; types with injected doubles. | Duplicating the same assertion in UI when only logic can break. |
| Integration | Real modules together: repo + in-memory DB, coordinator + stubbed network, DI wiring. | Accidentally turning into slow E2E; keep I/O stubbed or local. |
| UI (XCUITest) | Critical journeys: sign-in, checkout, onboarding; layout / a11y gaps lower layers miss. | Flakiness, long runs — accessibility IDs, stable backends. |
02
Designing for testability
SeamsSmallest useful seam
- Inject at init — time, network, analytics, flags — not globals.
- Extract pure logic (value types) from UIKit / SwiftUI effects.
- Protocols only where you substitute; avoid mega-factories.
Test doubles (roles)
- Stub — canned responses.
- Fake — working in-memory impl (e.g. fake repo).
- Spy / mock — record calls when you must assert interactions.
Injectable dependency
protocol Clock { var now: Date { get } }
struct SystemClock: Clock {
var now: Date { Date() }
}
final class SessionViewModel {
private let clock: Clock
init(clock: Clock) { self.clock = clock }
}
Object mother / factory
struct User: Equatable {
let id: String
let name: String
}
enum UserFactory {
static func make(
id: String = "u1",
name: String = "Ada"
) -> User { User(id: id, name: name) }
}
03
Async, concurrency & flakes
XCTest!
No
sleep as synchronization — inject clocks, await completions, assert invariants not scheduling order.Flaky vs deterministic
// Flaky — wall time
func testBad() {
sut.load()
Thread.sleep(forTimeInterval: 0.2)
XCTAssertEqual(sut.state, .ready)
}
// Deterministic — await real completion
func testGood() async {
await sut.load()
XCTAssertEqual(sut.state, .ready)
}
MainActor-isolated code
@MainActor
func testViewModel() async {
let vm = ProfileViewModel(service: StubService())
await vm.refresh()
XCTAssertFalse(vm.title.isEmpty)
}
// Or hop from a nonisolated async test:
func testHop() async {
await MainActor.run { /* touch UI-isolated types */ }
}
04
Healthy suite over time
CISpeed & tiers
- PR: fast unit + light integration (minutes).
- Main / nightly: broader integration, device UI smoke.
- Track duration per target; regressions become policy.
Reliability
- Quarantine flakes — fix or delete; don’t ignore forever.
- Shared factories + one stub catalog — less copy-paste drift.
- Assert behavior / contracts, not private implementation detail.
05
Speed vs confidence
Team✓
Test the right things at the right level — high confidence, minimal slowdown. Prioritize risky / business-critical code; not everything needs a test — focus on impact, not coverage % alone.
Principles
- Test behavior, not implementation.
- Fast + reliable over slow + brittle.
- Pyramid: more unit, fewer UI; CI keeps feedback continuous.
Outcomes
- Velocity + safety — developers trust green builds.
- Automation runs without blocking every keystroke.
- Invest where failures hurt users or revenue.