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.
fonte
Respostas:
Eu acho que testar métodos públicos é suficiente na maioria das vezes.
Se você possui muita complexidade em seus métodos particulares, considere colocá-los em outra classe como métodos públicos e use-os como chamadas particulares para esses métodos em sua classe original. Dessa forma, você pode garantir que os dois métodos nas classes original e utilitário funcionem corretamente.
Depender fortemente de métodos privados é algo a considerar sobre desicions de design.
fonte
Para a pergunta 1: isso depende. Normalmente, você começa com testes de unidade para métodos públicos. Às vezes, você encontra um método m que deseja manter privado para A, mas acha que faz sentido testar m isoladamente. Se for esse o caso, você deve tornar m público ou tornar a classe de teste
TestA
uma classe amigaA
. Mas tenha cuidado, adicionar um teste de unidade para testar m torna um pouco mais difícil alterar a assinatura ou o comportamento de m posteriormente; se você deseja manter esse "detalhe de implementação" de A, talvez seja melhor não adicionar um teste de unidade direto.Pergunta 2: A agregação composta (embutida em C ++) não funciona bem quando se trata de simular uma instância. De fato, como a construção do B acontece implicitamente no construtor de A, você não tem chance de injetar a dependência de fora. Se isso é um problema, depende da maneira como você deseja testar A: se você acha mais sensato testar A isoladamente, com uma simulação de B em vez de B, use melhor uma associação simples. Se você acha que pode escrever todos os testes de unidade necessários para A sem zombar de B, um composto provavelmente estará ok.
Pergunta 3: alterar uma API para tornar as coisas mais testáveis é comum, desde que você não tenha muito código até agora confiando nessa API. Quando você faz TDD, não precisa alterar sua API posteriormente para tornar as coisas mais testáveis; você começa inicialmente com uma API projetada para testabilidade. Se você quiser alterar uma API posteriormente para tornar as coisas mais testáveis, poderá encontrar problemas, isso é verdade. Então, eu usaria a primeira ou a segunda das alternativas que você descreveu, desde que você possa alterar sua API sem problemas, e usaria algo como a terceira alternativa (observação: isso funciona também sem o padrão singleton) apenas como último recurso, se for necessário Não altere a API sob nenhuma circunstância.
Sobre suas preocupações de que você pode "fazer errado": todo grande mecanismo ou máquina tem aberturas de manutenção, então IMHO a necessidade de adicionar algo assim ao software não é muito surpreendente.
fonte
Você deve procurar injeção de dependência e inversão de controle. Misko Hevery explica muito disso em seu blog. DI e IoC são efetivamente um padrão de design para criar código fácil de testar e simular.
Pergunta 1: Não, não torne públicos métodos privados para testá-los. Se o método for suficientemente complexo, você poderá criar uma classe de colaborador que contenha apenas esse método e injetá-lo (passar para o construtor) em sua outra classe. 1 1
Pergunta 2: Quem constrói A neste caso? Se você tem uma classe de fábrica / construtor que constrói A, não há mal nenhum em passar o colaborador B para o construtor. Se A estiver em um pacote / namespace separado do código que o utiliza, você pode tornar o construtor package-private e torná-lo para que o factory / builder seja a única classe que pode construí-lo.
Pergunta 3: Eu respondi isso na Pergunta 2. Algumas notas extras:
1 Esta é a resposta C # / Java - C ++ pode ter recursos extras que facilitam essa
Como resposta aos seus comentários:
O que eu quis dizer foi que seu código de produção seria alterado (por favor, perdoe meu pseudo-C ++):
para:
Então seu teste pode ser:
Dessa forma, você não precisa passar pela fábrica, o código que chama A não precisa saber como construir A e o teste pode construir A personalizado para permitir que o comportamento seja testado.
Em seu outro comentário, você perguntou sobre dependências aninhadas. Então, por exemplo, se suas dependências eram:
A primeira pergunta a fazer é se A usa C e D. Se não estão, por que estão incluídos em A? Supondo que eles sejam usados, talvez seja necessário passar em C em sua fábrica e fazer com que seu teste construa um MockC que retorne um MockB, permitindo testar todas as interações possíveis.
Se isso estiver começando a ficar complicado, pode ser um sinal de que seu design talvez esteja acoplado com muita força. Se você pode afrouxar o acoplamento e manter a coesão alta, esse tipo de DI fica mais fácil de implementar.
fonte