Senior / Staff iOS — interview prep

Testing

pyramid · XCTest · doubles · async · MainActor · CI · flakes · confidence

UnitIntegrationXCUITestStubFakeasync testsMainActorDeterminism
01

What to test where

Pyramid
i
Decide by risk and cost of failure — not a rigid ratio. More unit, some integration, very few UI tests.
LayerBest forWatch out
UnitPure logic, parsers, reducers, mappers; types with injected doubles.Duplicating the same assertion in UI when only logic can break.
IntegrationReal 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

Seams
Smallest 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

CI
Speed & 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.