Pruebas y calidad de código

Probar dependencias singleton mediante inyección del almacenamiento

Un protocolo y un doble de prueba permiten comprobar el pedido enviado, rechazar entradas inválidas y propagar fallos sin conectar una base de datos.

2 min de lectura
Imagen de portada de Probar dependencias singleton mediante inyección del almacenamiento

El obstáculo no es solo tener una instancia, sino elegir directamente una implementación concreta de almacenamiento. Expresa ese papel mediante un protocolo e inyecta la implementación para probar decisiones del servicio sin red.

Mecanismo y decisión de diseño

Recibir un objeto desde fuera no garantiza poder sustituirlo. Un parámetro de tipo concreto Database sigue imponiendo esa restricción al doble. Aquí OrderStore define un contrato pequeño y el servicio acepta any OrderStore. Un singleton también puede inyectarse si cumple el contrato. Fuente primaria

Código reproducible

El ejemplo completo define pedido, errores, protocolo, servicio y almacenamiento de registro. Comprueba el pedido exacto, rechaza un ID vacío antes de escribir y verifica que el fallo llegue al llamador. El doble combina un spy que registra llamadas con un stub de fallo configurable.

Este ejemplo independiente se ejecutó el 2026-10-23 con Apple Swift 6.4 para arm64 macOS, en modo de lenguaje Swift 6. No necesita red ni paquetes adicionales.

Descargar el archivo Swift completo

Ejecuta el comando en la carpeta de descarga. Las precondiciones detienen la ejecución si falla una comprobación. No uses -Ounchecked: elimina estas comprobaciones.

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")

Salida registrada e interpretación

saved=order-42 invalid-input-writes=0 storage-error=propagated

La solicitud correcta contiene order-42. Un ID vacío no añade llamadas y el error del almacenamiento no se descarta. Las comprobaciones cubren el valor transmitido y dos rutas de fallo, no solo el número de llamadas.

Alcance y limitaciones

Un protocolo no verifica transacciones, restricciones de unicidad ni comportamiento de red de la base real. Eso requiere pruebas de integración. No toda dependencia necesita abstracción, e inyectar estado global compartido no aísla automáticamente las pruebas.

Qué comprobar después

Empieza por un límite de E/S externa y un contrato pequeño. Comprueba que el almacenamiento real y el doble interpretan los errores igual; elige la implementación al ensamblar la aplicación. Separa los cambios de reglas de la refactorización.

Experimento relacionado

Fuentes y verificación