Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing.
From clarcnpx claudepluginhub marvinrichter/clarc --plugin clarcThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Patterns for making Swift code testable by abstracting external dependencies (file system, network, iCloud) behind small, focused protocols. Enables deterministic tests without I/O.
Each protocol handles exactly one external concern.
// File system access
public protocol FileSystemProviding: Sendable {
func containerURL(for purpose: Purpose) -> URL?
}
// File read/write operations
public protocol FileAccessorProviding: Sendable {
func read(from url: URL) throws -> Data
func write(_ data: Data, to url: URL) throws
func fileExists(at url: URL) -> Bool
}
// Bookmark storage (e.g., for sandboxed apps)
public protocol BookmarkStorageProviding: Sendable {
func saveBookmark(_ data: Data, for key: String) throws
func loadBookmark(for key: String) throws -> Data?
}
public struct DefaultFileSystemProvider: FileSystemProviding {
public init() {}
public func containerURL(for purpose: Purpose) -> URL? {
FileManager.default.url(forUbiquityContainerIdentifier: nil)
}
}
public struct DefaultFileAccessor: FileAccessorProviding {
public init() {}
public func read(from url: URL) throws -> Data {
try Data(contentsOf: url)
}
public func write(_ data: Data, to url: URL) throws {
try data.write(to: url, options: .atomic)
}
public func fileExists(at url: URL) -> Bool {
FileManager.default.fileExists(atPath: url.path)
}
}
public final class MockFileAccessor: FileAccessorProviding, @unchecked Sendable {
public var files: [URL: Data] = [:]
public var readError: Error?
public var writeError: Error?
public init() {}
public func read(from url: URL) throws -> Data {
if let error = readError { throw error }
guard let data = files[url] else {
throw CocoaError(.fileReadNoSuchFile)
}
return data
}
public func write(_ data: Data, to url: URL) throws {
if let error = writeError { throw error }
files[url] = data
}
public func fileExists(at url: URL) -> Bool {
files[url] != nil
}
}
Production code uses defaults; tests inject mocks.
public actor SyncManager {
private let fileSystem: FileSystemProviding
private let fileAccessor: FileAccessorProviding
public init(
fileSystem: FileSystemProviding = DefaultFileSystemProvider(),
fileAccessor: FileAccessorProviding = DefaultFileAccessor()
) {
self.fileSystem = fileSystem
self.fileAccessor = fileAccessor
}
public func sync() async throws {
guard let containerURL = fileSystem.containerURL(for: .sync) else {
throw SyncError.containerNotAvailable
}
let data = try fileAccessor.read(
from: containerURL.appendingPathComponent("data.json")
)
// Process data...
}
}
import Testing
@Test("Sync manager handles missing container")
func testMissingContainer() async {
let mockFileSystem = MockFileSystemProvider(containerURL: nil)
let manager = SyncManager(fileSystem: mockFileSystem)
await #expect(throws: SyncError.containerNotAvailable) {
try await manager.sync()
}
}
@Test("Sync manager reads data correctly")
func testReadData() async throws {
let mockFileAccessor = MockFileAccessor()
mockFileAccessor.files[testURL] = testData
let manager = SyncManager(fileAccessor: mockFileAccessor)
let result = try await manager.loadData()
#expect(result == expectedData)
}
@Test("Sync manager handles read errors gracefully")
func testReadError() async {
let mockFileAccessor = MockFileAccessor()
mockFileAccessor.readError = CocoaError(.fileReadCorruptFile)
let manager = SyncManager(fileAccessor: mockFileAccessor)
await #expect(throws: SyncError.self) {
try await manager.sync()
}
}
#if DEBUG conditionals instead of proper dependency injectionSendable conformance when used with actorsThe patterns in this skill are hexagonal (ports & adapters) and DDD — just in Swift idioms:
| Swift Pattern | Hexagonal / DDD Concept |
|---|---|
Small, focused protocol (e.g., FileAccessorProviding) | Output Port — defines what the domain needs |
DefaultFileAccessor (real implementation) | Outbound Adapter — production implementation |
MockFileAccessor (test implementation) | Test Adapter — injected in tests |
actor SyncManager (injects protocols) | Use Case / Application Service — orchestrates domain + ports |
Domain struct with let properties | Value Object — immutable, no framework imports |
Domain function throws domain errors | Domain behavior enforcing invariants |
// MARK: — Domain (no imports from UIKit, Foundation networking, SwiftData)
struct Market: Identifiable, Sendable {
let id: UUID
let name: String
let slug: String
let status: MarketStatus
}
enum MarketStatus: String, Sendable { case draft, active }
enum MarketError: Error {
case invalidName, alreadyPublished(String)
}
func createMarket(name: String, slug: String) throws -> Market {
guard !name.trimmingCharacters(in: .whitespaces).isEmpty else {
throw MarketError.invalidName
}
return Market(id: UUID(), name: name, slug: slug, status: .draft)
}
func publishMarket(_ market: Market) throws -> Market {
guard market.status == .draft else { throw MarketError.alreadyPublished(market.slug) }
return Market(id: market.id, name: market.name, slug: market.slug, status: .active)
}
// MARK: — Output Port (defined in domain)
protocol MarketRepository: Sendable {
func save(_ market: Market) async throws -> Market
func findBySlug(_ slug: String) async throws -> Market?
}
// MARK: — Use Case (depends only on port interface)
actor CreateMarketUseCase {
private let repository: any MarketRepository
init(repository: any MarketRepository) { // inject output port
self.repository = repository
}
func execute(name: String, slug: String) async throws -> Market {
let market = try createMarket(name: name, slug: slug) // domain logic
return try await repository.save(market) // via port
}
}
// MARK: — Outbound Adapter (production implementation)
actor CoreDataMarketRepository: MarketRepository {
// ... CoreData implementation, satisfies MarketRepository implicitly
}
// MARK: — Test Adapter (injected in tests)
final class MockMarketRepository: MarketRepository, @unchecked Sendable {
var saved: [Market] = []
func save(_ market: Market) async throws -> Market {
saved.append(market)
return market
}
func findBySlug(_ slug: String) async throws -> Market? { nil }
}
// MARK: — Test
@Test("CreateMarketUseCase saves a valid market")
func testCreateMarket() async throws {
let repo = MockMarketRepository()
let useCase = CreateMarketUseCase(repository: repo)
let market = try await useCase.execute(name: "Test", slug: "test")
#expect(market.name == "Test")
#expect(repo.saved.count == 1)
}
@Test("CreateMarketUseCase rejects blank name")
func testBlankName() async {
let useCase = CreateMarketUseCase(repository: MockMarketRepository())
await #expect(throws: MarketError.self) {
try await useCase.execute(name: "", slug: "slug")
}
}