Testes e qualidade de código

Por que Singleton bloqueia os testes e como resolvi isso com injeção de dependência

Você já passou a noite em claro porque o Singleton impedia escrever testes?

4 min de leitura
Imagem de capa de Por que Singleton bloqueia os testes e como resolvi isso com injeção de dependência

Você já passou a noite em claro porque o Singleton impedia escrever testes?

A lógica está correta, mas, ao adicionar testes, o código continua acessando o DB real e chamando a API real.

Vou começar pela conclusão.

Como o Singleton busca o objeto diretamente no código, não é possível substituí-lo por um objeto falso durante os testes.

Já a injeção de dependência recebe os objetos necessários de fora, permitindo trocá-los livremente nos testes.

Hoje vamos entender a diferença entre os dois usando código real.


Por que o Singleton bloqueia os testes?

Singleton é um objeto que existe apenas uma vez em todo o programa.

É prático: basta uma linha, Database.shared, para acessá-lo de qualquer lugar.

É aí que o problema começa.

Quando o objeto é buscado diretamente dentro de uma função, ela fica fortemente acoplada ao banco de dados real.

Durante os testes, não há como dizer: “Use um DB falso em vez do real”.

// Buscar o Singleton diretamente dentro da função
func saveOrder(_ order: Order) {
    // Fica fortemente acoplado ao DBreal
    let db = Database.shared
    db.insert(order) // Não há como impedir isso nos testes
}

Para testar esse código, o DB real precisa estar ligado.

Se o DB estiver desligado, o teste também falhará.

A lógica não está errada, mas a infraestrutura faz o teste ficar vermelho.


Como fazer injeção de dependência?

A injeção de dependência é menos complicada do que parece.

Em vez de buscar o objeto dentro da função, crie-o fora e passe-o como argumento.

Literalmente, você “injeta” o que é necessário.

// DBReceber de fora como parâmetro
func saveOrder(_ order: Order, db: Database) {
    db.insert(order) // Quem chama decide qual DBé
}

A diferença é de apenas uma linha, certo?

Basta trocar o acesso direto por Database.shared para receber o objeto como parâmetro.

Ainda assim, essa pequena mudança transforma completamente os testes.

No código real, você passa o DB real; nos testes, um DB falso.

Só mudamos o objeto fornecido, mas o resultado se divide assim
Só mudamos o objeto fornecido, mas o resultado se divide assim

Também é comum recebê-lo pelo construtor.

class OrderService {
    private let db: Database
    // Receber e guardar o objeto de fora ao criá-lo
    init(db: Database) { self.db = db }
}

Depois de recebê-lo, você pode usá-lo em qualquer lugar da classe como self.db, de forma muito mais limpa.


Substituindo por um objeto falso nos testes

Agora chegamos à parte realmente interessante.

Como a dependência vem de fora, nos testes basta passar um DB falso no lugar do real (normalmente chamado de mock).

@Test func Ao salvar_o pedido, ele é_DBentregue a_() {
    let fake = FakeDatabase() // Falso, não real
    saveOrder(Order(), db: fake)
    #expect(fake.savedCount == 1) // Verificar apenas se o salvamento foi chamado
}

Não é preciso ligar o DB real.

Você também não precisa de rede nem de servidor externo. Tudo termina instantaneamente na memória.

Com isso, os testes ficam mais rápidos e, principalmente, estáveis.

Assim, é possível validar apenas a lógica, sem depender das condições externas.

Depois de inserir o objeto falso, o teste ficou verde em um instante
Depois de inserir o objeto falso, o teste ficou verde em um instante

Singleton vs. injeção de dependência: uma comparação rápida

Resumi a diferença entre os dois nesta tabela.

Categoria Uso direto de Singleton Injeção de dependência
Onde o objeto é obtido Buscado diretamente dentro da função Passado de fora
Troca do objeto falso Praticamente impossível Pode ser feita livremente
Velocidade dos testes Lenta (exige recursos reais) Rápida (processada na memória)
Estabilidade dos testes Oscila conforme as condições externas Permite validar apenas a lógica
Acoplamento Alto Baixo

O Singleton, por si só, não é necessariamente ruim.

Ele é conveniente quando realmente só pode existir uma instância, como no caso de configurações.

O problema é o hábito de “buscar diretamente de qualquer lugar”. Isso amarra os testes.


Perguntas frequentes (Q&A)

P. Para fazer injeção de dependência, preciso obrigatoriamente de uma biblioteca de DI, como Swinject?

Não. Passar dependências por parâmetros ou pelo construtor, como no código de hoje, já é injeção de dependência. A biblioteca apenas gerencia isso automaticamente.

P. Não fica confuso quando há parâmetros demais?

Fica. Por isso, normalmente usamos injeção pelo construtor para receber tudo de uma vez ou agrupamos itens relacionados. Se os parâmetros continuam aumentando, pode ser um sinal de que a classe está fazendo coisas demais.

Inserir um DB falso no lugar do real: esse é o ponto central
Inserir um DB falso no lugar do real: esse é o ponto central

Se os testes continuam travados por recursos reais, mude apenas um ponto seguindo a abordagem de hoje.

Altere o código que busca objetos diretamente para recebê-los de fora.

Esse pequeno hábito torna os testes muito mais agradáveis de escrever. Bons códigos!

Continue lendo