Estou trabalhando em um projeto no qual temos que implementar e testar unidades algum novo módulo. Como eu tinha uma arquitetura bastante clara, escrevi rapidamente as principais classes e métodos e, em seguida, começamos a escrever testes de unidade.
Ao escrever os testes, tivemos que fazer algumas modificações no código original, como
- Tornar públicos métodos privados para testá-los
- Adicionando métodos extras para acessar variáveis privadas
- Adicionando métodos extras para injetar objetos simulados que devem ser usados quando o código é executado dentro de um teste de unidade.
De alguma forma, sinto que estes são sintomas de que estamos fazendo algo errado, por exemplo
- o design inicial estava errado (algumas funcionalidades deveriam ter sido públicas desde o início),
- o código não foi projetado adequadamente para fazer interface com testes de unidade (talvez devido ao fato de termos começado a projetar os testes de unidade quando algumas classes já haviam sido projetadas),
- estamos implementando testes de unidade da maneira errada (por exemplo, testes de unidade devem apenas testar / abordar diretamente os métodos públicos de uma API, não os privados),
- uma mistura dos três pontos acima, e talvez algumas questões adicionais em que não tenha pensado.
Como tenho alguma experiência com testes de unidade, mas estou longe de ser um guru, ficaria muito interessado em ler seus pensamentos sobre essas questões.
Além das perguntas gerais acima, tenho algumas questões técnicas mais específicas:
Pergunta 1. Faz sentido testar diretamente um método privado m de uma classe A e até torná-lo público para testá-lo? Ou devo assumir que m é indiretamente testado por testes de unidade que abrangem outros métodos públicos que chamam m?
Pergunta 2. Se uma instância da classe A contém uma instância da classe B (agregação composta), faz sentido zombar de B para testar A? Minha primeira idéia foi que eu não deveria zombar de B porque a instância B faz parte da instância A, mas comecei a duvidar disso. Meu argumento contra zombar de B é o mesmo que para 1: B é privado wrt A e é usado apenas para sua implementação, portanto zombando de B parece que estou expondo detalhes particulares de A como em (1). Mas talvez esses problemas indiquem uma falha de design: talvez não devamos usar agregação composta, mas uma associação simples de A a B.
Pergunta 3. No exemplo acima, se decidirmos zombar de B, como injetamos a instância B em A? Aqui estão algumas idéias que tivemos:
- Injete a instância B como um argumento para o construtor A em vez de criar a instância B no construtor A.
- Passe uma interface BFactory como argumento para o construtor A e deixe A usar a fábrica para criar sua instância B privada.
- Use um singleton BFactory privado para A. Use um método estático A :: setBFactory () para definir o singleton. Quando A deseja criar a instância B, ele usa o singleton de fábrica, se estiver definido (o cenário de teste), ele cria B diretamente se o singleton não estiver definido (o cenário de código de produção).
As duas primeiras alternativas me parecem mais limpas, mas exigem a alteração da assinatura do construtor A: alterar uma API apenas para torná-la mais testável me parece estranho, isso é uma prática comum?
O terceiro tem a vantagem de não exigir a alteração da assinatura do construtor (a alteração na API é menos invasiva), mas exige a chamada do método estático setBFactory () antes de iniciar o teste, que é propenso a erros da IMO ( dependência implícita de uma chamada de método para que os testes funcionem corretamente). Portanto, não sei qual devemos escolher.