O obstáculo não é apenas existir uma instância, mas o chamador escolher diretamente uma implementação concreta de armazenamento. Expresse esse papel em um protocolo e injete a implementação para testar decisões do serviço sem rede.
Mecanismo e decisão de projeto
Receber um objeto de fora não garante que ele possa ser substituído. Um parâmetro do tipo concreto Database ainda impõe essa restrição ao dublê. Aqui OrderStore define um contrato pequeno e o serviço aceita any OrderStore. Um singleton também pode ser injetado se cumprir o contrato. Fonte primária
Código reproduzível
O exemplo completo define pedido, erros, protocolo, serviço e armazenamento de registro. Verifica o pedido exato, rejeita um ID vazio antes da escrita e confirma que a falha chega ao chamador. O dublê combina um spy que registra chamadas com um stub de falha configurável.
Este exemplo independente foi executado em 2026-10-23 com Apple Swift 6.4 para arm64 macOS, no modo de linguagem Swift 6. Não requer rede nem pacotes adicionais.
Baixar o arquivo Swift completo
Execute o comando na pasta do download. As precondições interrompem a execução se uma verificação falhar. Não use -Ounchecked: essa opção remove as verificações.
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")
Saída registrada e interpretação
saved=order-42 invalid-input-writes=0 storage-error=propagated
A solicitação correta contém order-42. Um ID vazio não adiciona chamadas e o erro do armazenamento não é descartado. As verificações cobrem o valor enviado e dois caminhos de falha, não apenas a quantidade de chamadas.
Escopo e limitações
Um protocolo não verifica transações, restrições de unicidade nem o comportamento de rede do banco real. Isso exige testes de integração. Nem toda dependência precisa de abstração, e injetar estado global compartilhado não isola os testes automaticamente.
O que verificar em seguida
Comece por uma fronteira de E/S externa e um contrato pequeno. Confira se a implementação real e o dublê dão o mesmo significado aos erros; escolha a implementação ao montar a aplicação. Separe mudanças de regras da refatoração.
Fontes e verificação
- Inversion of Control Containers and the Dependency Injection PatternMartin Fowler · Fonte original do autor · Consultado 23 de outubro de 2026Evidência: Fonte do contrato de implementação e da explicação. A saída foi verificada separadamente com o experimento incluído aqui.

