Unidade testando uma classe que usa DI sem testar internamente

12

Eu tenho uma classe que é refatorada em 1 classe principal e 2 classes menores. As principais classes usam o banco de dados (como muitas das minhas classes) e envia um email. Portanto, a classe principal tem um IPersonRepositorye um IEmailRepositoryinjetado, que por sua vez envia para as 2 classes menores.

Agora, quero testar a unidade principal na classe principal e aprendi a não unir o funcionamento interno da classe, porque devemos poder mudar o funcionamento interno sem interromper os testes de unidade.

Mas como a classe usa oe IPersonRepositoryum IEmailRepository, EU TENHO que especificar (simulação / simulação) resultados para alguns métodos para o IPersonRepository. A classe principal calcula alguns dados com base nos dados existentes e os retorna. Se eu quiser testar isso, não vejo como posso escrever um teste sem especificar que o IPersonRepository.GetSavingsByCustomerIdretorno é x. Mas então meu teste de unidade 'sabe' sobre o funcionamento interno, porque 'sabe' quais métodos zombar e quais não.

Como posso testar uma classe que injetou dependências, sem o teste saber sobre os internos?

fundo:

Na minha experiência, muitos testes como esse criam simulações para os repositórios e, em seguida, fornecem os dados corretos para as simulações ou testam se um método específico foi chamado durante a execução. De qualquer forma, o teste conhece os internos.

Agora eu vi uma apresentação sobre a teoria (que eu ouvi antes) de que o teste não deveria saber sobre a implementação. Primeiro porque você não está testando como ele funciona, mas também porque, quando você altera a implementação, todos os testes de unidade falham porque eles 'sabem' sobre a implementação. Embora eu goste do conceito de que os testes desconhecem a implementação, não sei como realizá-la.

Michel
fonte
1
Assim que sua classe principal espera um IPersonRepositoryobjeto, essa interface e todos os métodos descritos não são mais "internos", portanto, não é realmente um problema do teste. Sua verdadeira pergunta deve ser "como refatorar classes em unidades menores sem expor muito em público". A resposta é "mantenha essas interfaces enxutas" (aderindo ao princípio de segmentação de interfaces, por exemplo). Esse é o ponto 2 do IMHO na resposta de @ DavidArno (acho que não há necessidade de repetir isso em outra resposta).
Doc Brown

Respostas:

7

A classe principal calcula alguns dados com base nos dados existentes e os retorna. Se eu quiser testar isso, não vejo como posso escrever um teste sem especificar que oIPersonRepository.GetSavingsByCustomerId retorno é x. Mas então meu teste de unidade 'sabe' sobre o funcionamento interno, porque 'sabe' quais métodos zombar e quais não.

Você está certo de que isso é uma violação do princípio "não teste internals" e é comum que as pessoas ignoram.

Como posso testar uma classe que injetou dependências, sem o teste saber sobre os internos?

Existem duas soluções que você pode adotar para solucionar essa violação:

1) Forneça uma simulação completa deIPersonRepository . Sua abordagem atualmente descrita é associar a simulação ao funcionamento interno do método em teste, simulando apenas os métodos que ele chamará. Se você fornecer zombarias para todos os métodos de IPersonRepository, remova esse acoplamento. O funcionamento interno pode mudar sem afetar a zombaria, tornando o teste menos frágil.

Essa abordagem tem a vantagem de manter o mecanismo de DI simples, mas pode criar muito trabalho se sua interface definir muitos métodos.

2) Não injete IPersonRepository, injete oGetSavingsByCustomerId método ou injete um valor de poupança. O problema de injetar implementações de interface inteiras é que você está injetando ("diga, não pergunte") e um sistema "pergunte, não diga", misturando as duas abordagens. Se você adotar uma abordagem de "DI puro", deve-se fornecer ao método (informado) o método exato a ser chamado se desejar um valor de poupança, em vez de receber um objeto (que deve ser solicitado efetivamente pelo método a ser chamado).

A vantagem dessa abordagem é que ela evita a necessidade de zombarias (além dos métodos de teste que você injeta no método em teste). A desvantagem é que isso pode fazer com que as assinaturas do método sejam alteradas em resposta aos requisitos da alteração do método.

Ambas as abordagens têm seus prós e contras, então escolha a que melhor se adapte às suas necessidades.

David Arno
fonte
1
Gosto muito da idéia número 2. Não sei por que não pensei nisso antes. Estou criando dependências no IRepositories há alguns anos, sem me questionar por que criei uma dependência para todo um IRepository (que em muitos casos conterá muitas assinaturas de método), enquanto ele depende apenas de uma ou 2 métodos. Portanto, em vez de injetar um IRepository, seria melhor injetar ou repassar a (única) assinatura do método necessária.
Michel
1

Minha abordagem é criar versões 'falsas' dos repositórios que leem de arquivos simples que contêm os dados necessários.

Isso significa que o teste individual não 'sabe' sobre a configuração do mock, embora obviamente o projeto geral do teste faça referência ao mock e tenha os arquivos de configuração etc.

Isso evita a configuração complexa de objetos simulados exigida pelas estruturas de simulação e permite que você use os objetos 'simulados' em instâncias reais do seu aplicativo para teste de UI e similares.

Como o 'mock' está totalmente implementado, em vez de ser configurado especificamente para o cenário de teste, uma alteração na implementação, por exemplo, digamos que GetSavingsForCustomer agora também deve excluir o cliente. Não interromperá os testes (a não ser, é claro, que realmente interrompa os testes), você precisará apenas atualizar sua implementação simulada e todos os seus testes serão executados sem alterar a configuração

Ewan
fonte
2
Isso ainda leva à situação em que eu crio uma simulação com dados para exatamente os métodos com os quais a classe testada está trabalhando. Ainda tenho o problema de que, quando a implementação for alterada, o teste será interrompido.
Michel
1
editado para explicar por que a minha abordagem é melhor, embora ainda não perfeita para o cenário eu acho que você quer dizer
Ewan
1

Testes de unidade são geralmente testes de caixa branca (você tem acesso ao código real). Portanto, não há problema em conhecer os internos até um certo ponto; no entanto, para iniciantes, é mais fácil não, porque você não deve testar o comportamento interno (como "chamar o método a primeiro, depois b e depois novamente").

Injetar uma simulação que fornece dados é aceitável, pois sua classe (unidade) depende desses dados externos (ou provedor de dados). No entanto, você deve verificar os resultados (não é o caminho para alcançá-los)! por exemplo, você fornece uma instância de Pessoa e verifica se um email foi enviado para o endereço de email correto; por exemplo, fornecendo uma simulação para o email também, essa simulação não faz nada além de armazenar o endereço de email do destinatário para acesso posterior pelo seu teste -código. (Acho que Martin Fowler os chama de Stubs em vez de Mocks, no entanto)

Andy
fonte