Ao fazer testes de unidade da maneira "adequada", ou seja, apagar todas as chamadas públicas e retornar valores predefinidos ou zombarias, sinto que não estou testando nada. Estou literalmente olhando meu código e criando exemplos com base no fluxo da lógica por meio de meus métodos públicos.
Parece que o método que você está testando precisa de várias outras instâncias de classe (que você precisa zombar) e chama vários métodos por conta própria.
Esse tipo de código é realmente difícil de testar por unidade, pelos motivos que você descreve.
O que eu achei útil é dividir essas classes em:
- Classes com a "lógica de negócios" real. Eles usam poucas ou nenhuma chamada para outras classes e são fáceis de testar (valor (es) dentro - valor fora).
- Classes que fazem interface com sistemas externos (arquivos, banco de dados etc.). Eles envolvem o sistema externo e fornecem uma interface conveniente para suas necessidades.
- Classes que "unem tudo"
Então as classes de 1. são fáceis de testar por unidade, porque elas apenas aceitam valores e retornam um resultado. Em casos mais complexos, essas classes podem precisar executar chamadas por conta própria, mas chamarão apenas as classes a partir de 2. (e não chamarão diretamente, por exemplo, uma função de banco de dados), e as classes a partir de 2. são fáceis de zombar (porque apenas expor as partes do sistema agrupado necessárias).
As classes de 2. e 3. geralmente não podem ser significativamente testadas por unidade (porque elas não fazem nada útil por si mesmas, são apenas códigos de "cola"). OTOH, essas classes tendem a ser relativamente simples (e poucas), portanto devem ser adequadamente cobertas por testes de integração.
Um exemplo
Uma classe
Digamos que você tenha uma classe que recupera um preço de um banco de dados, aplica alguns descontos e atualiza o banco de dados.
Se você tiver tudo isso em uma classe, precisará chamar funções de banco de dados, difíceis de zombar. No pseudocódigo:
1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database
Todas as três etapas precisarão de acesso ao banco de dados, portanto, muitas zombarias (complexas), que provavelmente serão interrompidas se o código ou a estrutura do banco de dados mudar.
Dividir
Você divide em três classes: PriceCalculation, PriceRepository, App.
O PriceCalculation faz apenas o cálculo real e recebe os valores necessários. App une tudo:
App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices
Dessa maneira:
- PriceCalculation encapsula a "lógica de negócios". É fácil testar porque não chama nada por si só.
- O PriceRepository pode ser testado por pseudo-unidade configurando um banco de dados falso e testando as chamadas de leitura e atualização. Ele tem pouca lógica e, portanto, poucos caminhos de código, portanto você não precisa de muitos desses testes.
- O aplicativo não pode ser significativamente testado em unidade, porque é um código de cola. No entanto, também é muito simples, portanto, o teste de integração deve ser suficiente. Se o aplicativo posterior se tornar muito complexo, você quebrará mais classes de "lógica de negócios".
Por fim, pode acontecer que o PriceCalculation faça suas próprias chamadas ao banco de dados. Por exemplo, porque apenas o PriceCalculation sabe quais dados são necessários e, portanto, não pode ser buscado antecipadamente pelo aplicativo. Em seguida, você pode transmitir uma instância do PriceRepository (ou alguma outra classe de repositório), personalizada de acordo com as necessidades do PriceCalculation. Essa classe precisará ser ridicularizada, mas isso será simples, porque a interface do PriceRepository é simples, por exemplo PriceRepository.getPrice(articleNo, contractType)
. Mais importante ainda, a interface do PriceRepository isola o PriceCalculation do banco de dados, portanto, é improvável que as alterações no esquema do banco de dados ou na organização de dados alterem sua interface e, portanto, quebrem as zombarias.