Novo no teste de unidade, como escrever ótimos testes? [fechadas]

267

Sou bastante novo no mundo dos testes de unidade e resolvi adicionar uma cobertura de teste para o meu aplicativo existente esta semana.

Essa é uma tarefa enorme, principalmente por causa do número de classes a serem testadas, mas também porque escrever testes é tudo novo para mim.

Já escrevi testes para várias classes, mas agora estou me perguntando se estou fazendo certo.

Quando estou escrevendo testes para um método, tenho a sensação de reescrever uma segunda vez o que já escrevi no próprio método.
Meus testes parecem tão intimamente ligados ao método (testando todo o caminho de código, esperando que alguns métodos internos sejam chamados várias vezes, com certos argumentos), que parece que, se eu refatorar o método, os testes falharão mesmo que o o comportamento final do método não mudou.

Este é apenas um sentimento e, como já foi dito, não tenho experiência em testes. Se alguns testadores mais experientes do mercado pudessem me dar conselhos sobre como escrever ótimos testes para um aplicativo existente, isso seria muito apreciado.

Edit: Eu adoraria agradecer ao Stack Overflow, tive ótimas contribuições em menos de 15 minutos que responderam mais das horas de leitura on-line que acabei de fazer.

pixelastic
fonte
1
Este é o melhor livro para testes de unidade: manning.com/osherove Explica todas as práticas recomendadas, faça e não faça para testes de unidade.
Ervi B
Uma coisa que todas essas respostas deixam de fora é que o teste de unidade é como documentação. Portanto, se você escrever uma função, documentaria sua intenção, descrevendo suas entradas e saídas (e, possivelmente, efeitos colaterais). Um teste de unidade serve para verificar isso, então. E se você (ou outra pessoa) posteriormente fizer alterações no código, os documentos deverão explicar os limites de quais alterações podem ser feitas e os testes de unidade garantirão que os limites sejam mantidos.
Thomas Tempelmann

Respostas:

187

Meus testes parecem tão intimamente ligados ao método (testando todo o caminho de código, esperando que alguns métodos internos sejam chamados várias vezes, com certos argumentos), que parece que, se eu refatorar o método, os testes falharão mesmo que o o comportamento final do método não mudou.

Eu acho que você está fazendo errado.

Um teste de unidade deve:

  • testar um método
  • fornecer alguns argumentos específicos para esse método
  • testar se o resultado é o esperado

Ele não deve olhar dentro do método para ver o que está fazendo; portanto, alterar os internos não deve causar falhas no teste. Você não deve testar diretamente se métodos particulares estão sendo chamados. Se você estiver interessado em descobrir se seu código privado está sendo testado, use uma ferramenta de cobertura de código. Mas não fique obcecado com isso: 100% de cobertura não é um requisito.

Se o seu método chama métodos públicos em outras classes, e essas chamadas são garantidas pela sua interface, você pode testar se essas chamadas estão sendo feitas usando uma estrutura de simulação.

Você não deve usar o próprio método (ou qualquer código interno que ele usa) para gerar o resultado esperado dinamicamente. O resultado esperado deve ser codificado no seu caso de teste para que não seja alterado quando a implementação for alterada. Aqui está um exemplo simplificado do que um teste de unidade deve fazer:

testAdd()
{
    int x = 5;
    int y = -2;
    int expectedResult = 3;
    Calculator calculator = new Calculator();
    int actualResult = calculator.Add(x, y);
    Assert.AreEqual(expectedResult, actualResult);
}

Observe que a forma como o resultado é calculado não é verificada - apenas que o resultado está correto. Continue adicionando cada vez mais casos de teste simples, como o descrito acima, até ter coberto o maior número possível de cenários. Use sua ferramenta de cobertura de código para verificar se você perdeu algum caminho interessante.

Mark Byers
fonte
13
Muito obrigado, sua resposta foi a mais completa. Agora entendo melhor para que servem realmente os objetos simulados: não preciso afirmar todas as chamadas para outros métodos, apenas os relevantes. Também não preciso saber como as coisas são feitas, mas o que elas fazem corretamente.
pixelastic
2
Eu respeitosamente acho que você está fazendo errado. Os testes de unidade são sobre o fluxo de execução de código (teste de caixa branca). O teste da caixa preta (o que você está sugerindo) geralmente é a técnica usada no teste funcional (teste de sistema e integração).
8287 Wes
1
"Um teste de unidade deve testar um método", na verdade discordo. Um teste de unidade deve testar um conceito lógico. Enquanto que muitas vezes é representado como um método, que nem sempre é o caso
robertmain
35

Para testes de unidade, achei o Test Driven (testes primeiro, código segundo) e o código primeiro, teste segundo como extremamente úteis.

Em vez de escrever código, escreva teste. Escreva o código e veja o que você acha que o código deveria estar fazendo. Pense em todos os usos pretendidos e, em seguida, escreva um teste para cada um. Acho os testes de escrita mais rápidos, mas mais envolvidos do que a própria codificação. Os testes devem testar a intenção. Também pense nas intenções de encontrar casos de esquina na fase de redação do teste. E, é claro, ao escrever testes, você pode encontrar um dos poucos usos que causa um bug (algo que muitas vezes encontro, e estou muito feliz que esse bug não corrompeu os dados e não foi verificado).

No entanto, testar é quase como codificar duas vezes. Na verdade, eu tinha aplicativos em que havia mais código de teste (quantidade) do que código de aplicativo. Um exemplo foi uma máquina de estado muito complexa. Eu tinha que ter certeza de que, depois de adicionar mais lógica, a coisa toda sempre funcionava em todos os casos de uso anteriores. E como esses casos eram bastante difíceis de acompanhar, observando o código, acabei tendo um conjunto de testes tão bom para esta máquina que estava confiante de que ela não quebraria mesmo depois de fazer alterações, e os testes salvaram minha bunda algumas vezes . E como os usuários ou testadores estavam encontrando bugs com os casos de fluxo ou de canto inexplicáveis, adivinhem, foram adicionados aos testes e nunca mais aconteceram. Isso realmente deu aos usuários confiança no meu trabalho, além de tornar a coisa toda super estável. E quando teve que ser reescrito por razões de desempenho, adivinhe,

Todos os exemplos simples function square(number)são ótimos e provavelmente são maus candidatos para passar muito tempo testando. Aqueles que fazem lógica comercial importante, é aí que os testes são importantes. Teste os requisitos. Não basta testar o encanamento. Se os requisitos mudarem, adivinhem, os testes também devem.

O teste não deve estar literalmente testando se a função invocou a barra de função 3 vezes. Isso esta errado. Verifique se o resultado e os efeitos colaterais estão corretos, não a mecânica interna.

Dmitriy Likhten
fonte
2
Boa resposta, me deu confiança de que escrever testes após o código ainda pode ser útil e possível.
pixelastic
2
Um exemplo recente perfeito. Eu tinha uma função muito simples. Passe-o verdadeiro, faz uma coisa, falso faz outra. MUITO SIMPLES. Tive como 4 testes verificando para garantir que a função faça o que pretende fazer. Eu mudo um pouco o comportamento. Executar testes, POW um problema. O engraçado é que, ao usar o aplicativo, o problema não se manifesta, é apenas em um caso complexo. O caso de teste encontrou e eu economizei horas de dor de cabeça.
Dmitriy Likhten 08/08/10
"Os testes devem testar a intenção." Eu acho que resume tudo, que você deve passar pelos usos pretendidos do código e garantir que o código possa acomodá-los. Também aponta para o escopo do que o teste realmente deve testar e a idéia de que, quando você faz uma alteração no código, no momento você não pode considerar mais adiante como essa mudança afeta todos os usos prescritos do código - o teste se defende contra uma mudança que não satisfaz todos os casos de uso pretendidos.
Greenstick 6/12/19
18

Vale a pena notar que os testes de unidade de adaptação automática no código existente são muito mais difíceis do que conduzir a criação desse código com testes em primeiro lugar. Essa é uma das grandes questões ao lidar com aplicativos herdados ... como fazer o teste de unidade? Isso já foi solicitado muitas vezes antes (para que você possa ser fechado como uma pergunta idiota), e as pessoas geralmente acabam aqui:

Movendo o código existente para o Test Driven Development

Eu apóio a recomendação do livro da resposta aceita, mas além disso há mais informações vinculadas nas respostas.

David
fonte
3
Se você escreve testes primeiro ou segundo, tudo bem, mas ao escrever testes, você garante que seu código é testável para que você possa escrever testes. Você acaba pensando "como posso testar isso" com frequência, que por si só faz com que um código melhor seja escrito. A atualização de casos de teste é sempre um grande não-não. Muito difícil. Não é um problema de tempo, é um problema de quantidade e testabilidade. Não posso falar com meu chefe agora e dizer que quero escrever casos de teste para mais de mil tabelas e usos, agora é demais, levaria um ano e algumas das lógicas / decisões foram esquecidas. Portanto, não
adie
2
Presumivelmente, a resposta aceita mudou. Há uma resposta da Linx que recomenda The art of unit testing por Roy Osherove, manning.com/osherove
thelem
15

Não escreva testes para obter uma cobertura completa do seu código. Escreva testes que garantam seus requisitos. Você pode descobrir caminhos de código desnecessários. Por outro lado, se necessário, eles estão lá para atender a algum tipo de requisito; encontre o que é e teste o requisito (não o caminho).

Mantenha seus testes pequenos: um teste por requisito.

Mais tarde, quando precisar fazer uma alteração (ou escrever um novo código), tente escrever um teste primeiro. Apenas um. Depois, você terá dado o primeiro passo no desenvolvimento orientado a testes.

Jon Reid
fonte
Obrigado, faz sentido ter apenas pequenos testes para pequenos requisitos, um de cada vez. Lição aprendida.
pixelastic
13

O teste de unidade é sobre a saída que você obtém de uma função / método / aplicativo. Não importa como o resultado é produzido, apenas importa que esteja correto. Portanto, sua abordagem de contagem de chamadas para métodos internos está errada. O que costumo fazer é sentar e escrever o que um método deve retornar, com base em certos valores de entrada ou em um determinado ambiente e, em seguida, escrever um teste que compare o valor real retornado com o que eu criei.

fresskoma
fonte
Obrigado ! Eu tinha a sensação de que estava fazendo errado, mas ter alguém realmente me dizendo é melhor.
22410 pixelastic
8

Tente escrever um teste de unidade antes de escrever o método a ser testado.

Definitivamente, isso forçará você a pensar um pouco diferente sobre como as coisas estão sendo feitas. Você não tem idéia de como o método vai funcionar, exatamente o que ele deve fazer.

Você sempre deve testar os resultados do método, não como o método obtém esses resultados.

Justin Niessner
fonte
Sim, eu adoraria poder fazer isso, exceto que os métodos já estão escritos. Eu só quero testá-los. Escreverei testes antes dos métodos no futuro, também.
22410 pixelastic
2
@pixelastic finge que os métodos não foram escritos?
committedandroider
4

os testes devem melhorar a manutenção. Se você alterar um método e um teste for interrompido, isso pode ser uma coisa boa. Por outro lado, se você olhar para o seu método como uma caixa preta, não deve importar o que está dentro do método. O fato é que você precisa zombar de alguns testes e, nesses casos, você realmente não pode tratar o método como uma caixa preta. A única coisa que você pode fazer é escrever um teste de integração - você carrega uma instância totalmente instanciada do serviço em teste e faz com que ele faça o mesmo que faria em seu aplicativo. Então você pode tratá-lo como uma caixa preta.

When I'm writing tests for a method, I have the feeling of rewriting a second time what I          
already wrote in the method itself.
My tests just seems so tightly bound to the method (testing all codepath, expecting some    
inner methods to be called a number of times, with certain arguments), that it seems that
if I ever refactor the method, the tests will fail even if the final behavior of the   
method did not change.

Isso ocorre porque você está escrevendo seus testes depois de escrever seu código. Se você fizesse o contrário (escrevesse os testes primeiro), não seria assim.

hvgotcodes
fonte
Obrigado pelo exemplo da caixa preta, não pensei assim. Gostaria de ter descoberto o teste de unidade anteriormente, mas, infelizmente, não é esse o caso e estou preso a um aplicativo herdado ao qual adicionar testes. Não há como adicionar testes a um projeto existente sem que eles se sintam quebrados?
pixelastic
1
Escrever testes depois é diferente de escrever testes antes, então você está preso a ele. no entanto, o que você pode fazer é configurar os testes para que eles falhem primeiro e depois fazê-los passar colocando sua classe em teste ... faça algo assim, colocando sua instância em teste após o teste falhar inicialmente. O mesmo acontece com as zombarias - inicialmente o mock não tem expectativas e falhará porque o método em teste fará algo com o mock e fará o teste passar. Eu não ficaria surpreso se você encontrar muitos erros dessa maneira.
hvgotcodes
Além disso, seja realmente específico com suas expectativas. Não afirme apenas que o teste retorna um objeto, teste se o objeto possui vários valores. Teste se, quando um valor deve ser nulo, é esse. Você também pode dividir um pouco refatorando o que pretendia fazer, depois de adicionar alguns testes.
hvgotcodes