测试与代码质量

注入存储接口,验证单例依赖的成功与失败

使用存储协议和记录型测试替身,在不连接数据库的情况下验证订单内容、写入前的输入拒绝,以及存储错误的传递。

2 分钟阅读
注入存储接口,验证单例依赖的成功与失败 封面图

障碍不只是只有一个实例,而是调用方直接选择具体存储实现。把存储角色表达为协议并注入实现,就能在没有网络的情况下检查服务的判断。

原理与设计判断

从外部接收对象,不代表一定能够替换实现。如果参数仍是具体Database类型,替身也必须满足同一类型约束。这里用OrderStore建立小型契约,服务接收any OrderStore。符合契约的单例也可以被注入。 一手资料

复现代码

完整示例定义了订单、错误、协议、服务及记录型存储替身。测试正常传入的订单值、空ID是否在写入前被拒绝,以及存储失败是否传到调用方。替身兼有记录调用的spy和配置失败行为的stub角色。

这个独立示例已于 2026-10-23 使用 Apple Swift 6.4,以 Swift 6 语言模式在 arm64 macOS 目标环境运行。不需要网络或额外软件包。

下载完整 Swift 文件

请在下载文件的文件夹中运行。检查不通过时,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边界开始建立小型契约。确认正式存储实现与替身对错误的定义一致,再于组装应用时选择实现。业务规则变更请与保留行为的重构分开处理。

延伸实验

来源与验证