Skip to content
Prepare to Grow

iOS Architecture

Testing Architectural Boundaries

Use Swift Testing, focused fakes, and a balanced test strategy to verify behavior without coupling tests to implementation details.

Advanced7 min read2 guided activities

Before you begin

What you will understand

  • Choose unit, integration, UI, and performance tests by risk.
  • Design fakes, stubs, spies, and deterministic dependencies.
  • Test behavior through stable boundaries.
  • Use Swift Testing for asynchronous and parameterized scenarios.
01

Learning stage

Concept Explanation

Testability is architectural feedback. If a business rule requires a simulator, network, database, and navigation stack before it can be verified, responsibilities are probably too tightly connected. Good boundaries let fast tests exercise policy directly while a smaller number of integration and UI tests verify that real pieces connect and users can complete important workflows.

A fake is a lightweight working implementation, such as an in-memory repository. A stub returns controlled values, while a spy records interactions for later verification. Prefer state or returned-result assertions over verifying every internal method call. A mock with rigid interaction expectations can make harmless refactoring expensive when the interaction itself is not the behavior under test.

Swift Testing provides @Test, #expect, #require, parameterized tests, traits, and native concurrency support for unit and integration tests. XCTest remains important for UI automation and established suites. Inject clocks, identifiers, repositories, and failure behavior so tests are deterministic, isolated, and safe to run in parallel.

  • Unit test: fast verification of focused logic in isolation.
  • Integration test: verifies cooperating real components at a boundary.
  • UI test: validates a critical workflow through the application interface.
  • Fake: simplified working implementation.
  • Stub: controlled provider of predetermined output.
  • Spy: records meaningful interactions for assertions.
02

Learning stage

Syntax & Examples

An actor-backed fake and fixed clock verify a use case without networking, persistence, waiting, or global state.

CompleteLessonTests.swiftswift
import Foundation
import Testing
@testable import LearningDomain

actor ProgressStoreFake: ProgressSaving {
  private var stored: [LessonProgress] = []

  func save(_ progress: LessonProgress) async throws {
    stored.append(progress)
  }

  func values() -> [LessonProgress] { stored }
}

@Suite("Complete lesson")
struct CompleteLessonTests {
  @Test("Records the selected lesson with the current date")
  func recordsProgress() async throws {
    let store = ProgressStoreFake()
    let date = Date(timeIntervalSince1970: 1_800_000_000)
    let lessonID = UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE")!
    let completeLesson = CompleteLesson(
      progressStore: store,
      now: { date }
    )

    try await completeLesson(lessonID: lessonID)

    let saved = await store.values()
    #expect(saved == [LessonProgress(lessonID: lessonID, completedAt: date)])
  }
}

@Test("Certificates require every lesson", arguments: [0, 3, 4])
func certificateRule(completed: Int) {
  let course = Course(
    id: UUID(uuidString: "11111111-2222-3333-4444-555555555555")!,
    title: "Architecture",
    completedLessons: completed,
    totalLessons: 4
  )
  #expect(course.canReceiveCertificate == (completed == 4))
}
The tests control time and storage, assert observable outcomes, and parameterize a domain rule over representative boundaries.
03

Learning stage

Real-World Usage

Connect the architectural concept to maintainable iOS features and real change scenarios.

  • Test reducers and view models across loading, success, empty, error, retry, and cancellation paths.
  • Verify repository integration against an isolated temporary store and stub transport.
  • Protect sign-in, purchase, editing, and restoration with a small set of UI workflows.
  • Run feature-unit tests frequently and broader plans before integration or release.
04

Learning stage

Common Mistakes

Identify abstractions and dependencies that add coupling, ambiguity, or unnecessary ceremony.

  • Equating code coverage percentage with useful confidence.
  • Mocking every collaborator and asserting private call order.
  • Sharing mutable test state while tests run in parallel.
  • Using arbitrary sleeps for asynchronous behavior.
  • Relying only on UI tests for business rules and edge cases.
05

Learning stage

Practice Exercises

Apply the design to a small feature and evaluate it through observable change scenarios.

Activity 1

Design a feature test strategy

For a course editor, assign validation, saving, repository mapping, navigation, accessibility, and full creation flow to unit, integration, UI, or performance tests.

Hint: Use the lowest-cost test that observes the risk, then add a few high-fidelity workflows.

Activity 2

Remove nondeterminism

Refactor a test that depends on Date(), UUID(), a live API, and Task.sleep so each external input is controlled.

Hint: Inject tiny closures when a full protocol would add no value.

06

Learning stage

Knowledge Check

Answer each question before revealing the explanation.

QUESTION 1

What distinguishes a fake from a stub?

  1. A.A fake has a simplified working implementation; a stub returns controlled output
  2. B.A fake always uses production servers
  3. C.A stub must render UI
  4. D.There is no useful distinction
Reveal answer

A fake has a simplified working implementation; a stub returns controlled output

Both are test doubles, but their behavior and intended use differ.

QUESTION 2

Why not verify every rule through UI tests?

Reveal answer

UI tests are slower and more variable, and they provide less focused failure feedback than direct unit tests.

Use UI tests for critical user workflows and faster tests for detailed logic and edge cases.

07

Learning stage

Interview Preparation

Explain the decision, dependency direction, trade-offs, and evidence—not only the pattern name.

How does architecture influence your testing strategy?

Short answer: Clear boundaries make important rules directly testable and reserve integration or UI tests for connections and user workflows.

Explore the detailed answer

I test domain transitions and presentation state with fast Swift Testing tests, use fakes or stubs at external boundaries, add integration tests for adapters and persistence, and keep UI tests focused on high-value flows. I inject nondeterministic values, assert observable behavior instead of private implementation, and use test plans to balance local feedback with broader confidence.

Possible follow-ups

  • • When would you use a spy?
  • • How do you test cancellation and stale results?
08

Learning stage

Key Takeaways

A healthy architecture creates fast, deterministic seams for policy tests while preserving a smaller number of realistic integration and UI checks.

  • Treat difficult testing as coupling feedback.
  • Choose test level by risk and feedback cost.
  • Use the simplest suitable test double.
  • Inject nondeterministic inputs.
  • Assert stable behavior rather than private call sequences.