障碍不只是只有一个实例,而是调用方直接选择具体存储实现。把存储角色表达为协议并注入实现,就能在没有网络的情况下检查服务的判断。
原理与设计判断
从外部接收对象,不代表一定能够替换实现。如果参数仍是具体Database类型,替身也必须满足同一类型约束。这里用OrderStore建立小型契约,服务接收any OrderStore。符合契约的单例也可以被注入。 一手资料
复现代码
完整示例定义了订单、错误、协议、服务及记录型存储替身。测试正常传入的订单值、空ID是否在写入前被拒绝,以及存储失败是否传到调用方。替身兼有记录调用的spy和配置失败行为的stub角色。
这个独立示例已于 2026-10-23 使用 Apple Swift 6.4,以 Swift 6 语言模式在 arm64 macOS 目标环境运行。不需要网络或额外软件包。
请在下载文件的文件夹中运行。检查不通过时,precondition 会终止程序。请勿使用 -Ounchecked,因为它会移除这些检查。
swift -swift-version 6 singleton-testing-dependency-injection.swift
struct Order: Equatable { let id: String }
enum SaveError: Error { case invalidID, unavailable }
protocol OrderStore {
func insert(_ order: Order) throws
}
struct OrderService {
let store: any OrderStore
func save(_ order: Order) throws {
guard !order.id.isEmpty else { throw SaveError.invalidID }
try store.insert(order)
}
}
final class RecordingStore: OrderStore {
var calls: [Order] = []
var shouldFail = false
func insert(_ order: Order) throws {
calls.append(order)
if shouldFail { throw SaveError.unavailable }
}
}
let store = RecordingStore()
let service = OrderService(store: store)
let order = Order(id: "order-42")
try service.save(order)
precondition(store.calls == [order])
do {
try service.save(Order(id: ""))
preconditionFailure("invalid input was accepted")
} catch SaveError.invalidID {}
precondition(store.calls.count == 1)
store.shouldFail = true
do {
try service.save(order)
preconditionFailure("storage failure was swallowed")
} catch SaveError.unavailable {}
precondition(store.calls.count == 2)
print("saved=order-42 invalid-input-writes=0 storage-error=propagated")
运行结果与解读
saved=order-42 invalid-input-writes=0 storage-error=propagated
正常请求传入order-42。空ID没有增加存储调用,存储错误也没有被吞掉。除了调用次数,这里还检查传入值和两种失败路径。
适用范围与限制
协议不能证明真实数据库的事务、唯一性约束或网络行为正确,仍须另做集成测试。不是所有依赖都需要抽象化,注入全局共享状态也不会自动让测试彼此隔离。
接下来要确认的事
从一个外部I/O边界开始建立小型契约。确认正式存储实现与替身对错误的定义一致,再于组装应用时选择实现。业务规则变更请与保留行为的重构分开处理。
来源与验证
- Inversion of Control Containers and the Dependency Injection PatternMartin Fowler · 作者原文 · 核查 2026年10月23日依据: 实现契约与说明的依据。运行输出另由本文所附实验确认。

