Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

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/enum can 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 dependency injection
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.

Pure function unit test
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")
}