Skip to content

christopherkmoore/TestabilityWorkshop

Repository files navigation

Why Testable Code Matters

TestabilityWorkshop

An Xcode project covering the core patterns for writing testable iOS code — dependency injection, protocol mocking, abstracting side effects, and async testing. Each exercise shows the untestable pattern first, then the refactored version, then the tests that become possible.

Companion to the Why Testable Code Matters series on pixelper.com.

Requires: Xcode 15+, iOS 17+


Getting Started

git clone https://github.com/christopherkmoore/TestabilityWorkshop.git
cd TestabilityWorkshop
open TestabilityWorkshop.xcodeproj

Or regenerate with XcodeGen:

brew install xcodegen && xcodegen generate

Exercises

Dependency Injection

Singleton Refactoring — A UserProfileManager that directly accesses AnalyticsServiceSingleton.shared, AuthenticationManagerSingleton.shared, and APIClientSingleton.shared. Refactored version extracts protocols for each; singletons remain the production defaults but tests pass mocks.

Refactoring Singletons for Testability

Decoupling Network Code — A ProductService that uses URLSession.shared directly vs. one that accepts an HTTPClient protocol. The mock records requested URLs and returns configurable responses — tests run instantly with no network.

Decoupling Network Code · Building Mock HTTP Clients

Testing Time-Dependent Code — A SubscriptionManager that calls Date() directly, making expiry tests impossible. Refactored to accept a DateProviding protocol. MockDateProvider has a mutable now — set it to anything and test deterministically.

Testing Time-Dependent Code · DateProviding: Control Time in Tests


Side Effects

UserDefaults — An OnboardingManager that reads/writes UserDefaults.standard directly vs. one that accepts a KeyValueStore protocol. InMemoryKeyValueStore stores in a dictionary — tests are isolated and don't touch real app data.

Why UserDefaults.standard Breaks Your Tests · InMemoryKeyValueStore Pattern

File System — An ImageCacheManager that calls FileManager.default directly vs. one that accepts a FileSystemProtocol. InMemoryFileSystem stores files as [String: Data] — tests run without touching disk.

Abstracting File System Access · In-Memory File Systems for Tests


Architecture

ViewModels with @Published — Before: ArticleListViewModel owns a URLSession internally. After: accepts an ArticleRepository protocol. Tests observe @Published state transitions using Combine and a LoadingState enum.

Repository Pattern for Testable ViewModels · Testing ViewModels with @Published

Mock Patterns — An OrderService that orchestrates an EmailService, PaymentProcessor, and NotificationScheduler. Implementations for each: a spy mock that records calls, a stub that returns configurable responses, and a fake with in-memory state. Tests verify interaction sequences, not just return values.

Mock vs Stub vs Fake vs Spy · Verifying Interactions with Spy Mocks · Building Testable Service Layers


Async Testing

Async/Await — A WeatherManager with caching and a SearchManager actor with debounce and cancellation. MockWeatherService has controllable delay and error injection. Tests cover cache hits, concurrent fetches, and cancelled searches.

Testing Async/Await Methods · Testing TaskGroup Operations · Actor Isolation in Unit Tests


Swift Testing

Side-by-side comparisons of XCTest and Swift Testing, covering #expect, parameterized tests with @Test(arguments:), tags, traits (.disabled, .timeLimit, .bug), @Suite, and confirmation() for async callbacks.

XCTest vs Swift Testing: The Modern Approach · Swift Testing Killer Features · Setup and Teardown: Just Use init() · Swift Testing Attributes: The Complete Reference


pixelper.com · Blog

About

iOS unit testing exercises — dependency injection, mocking, protocol-based testing patterns

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages