Como escrever testes de unidade "bons"?

61

Acionado por esse segmento , eu (novamente) estou pensando em finalmente usar testes de unidade em meus projetos. Alguns pôsteres dizem algo como "Os testes são legais, se são bons testes". Minha pergunta agora: o que são testes "bons"?

Em meus aplicativos, a parte principal geralmente é algum tipo de análise numérica, dependendo de grandes quantidades de dados observados e resultando em uma função de ajuste que pode ser usada para modelar esses dados. Achei especialmente difícil construir testes para esses métodos, uma vez que o número de entradas e resultados possíveis é muito grande para testar todos os casos, e os métodos em si são geralmente muito longos e não podem ser facilmente refatorados sem sacrificar o desempenho. Estou especialmente interessado em "bons" testes para esse tipo de método.

Jens
fonte
8
Qualquer bom teste de unidade deve testar apenas uma coisa - se falhar, você deve saber exatamente o que deu errado.
gablin
2
Ao ter grandes quantidades de dados, o bom é escrever testes genéricos que podem receber arquivos de dados como entrada. Os arquivos de dados normalmente devem conter tanto a entrada quanto o resultado esperado. Com estruturas de teste xunit, você pode gerar casos de teste em tempo real - um para cada amostra de dados.
Frodeik
2
@gablin "Se falhar, você deve saber exatamente o que deu errado" sugeriria que os testes com várias causas possíveis de falha estão bem, desde que você possa determinar a causa a partir da saída do teste ...?
user253751
Ninguém parece ter mencionado que os testes de unidade podem testar quanto tempo leva a operação. Você pode refatorar seu código com o desempenho em mente, garantindo que o teste de unidade informe se ele passa ou falha com base no tempo e nos resultados.
CJ Dennis

Respostas:

52

A Arte do Teste de Unidade tem o seguinte a dizer sobre testes de unidade:

Um teste de unidade deve ter as seguintes propriedades:

  • Deve ser automatizado e repetível.
  • Deve ser fácil de implementar.
  • Uma vez escrito, ele deve permanecer para uso futuro.
  • Qualquer pessoa deve ser capaz de executá-lo.
  • Deve ser executado com o pressionar de um botão.
  • Deve ser executado rapidamente.

e depois acrescenta que deve ser totalmente automatizado, confiável, legível e sustentável.

Eu recomendo fortemente a leitura deste livro, se você ainda não o fez.

Na minha opinião, tudo isso é muito importante, mas os três últimos (confiáveis, legíveis e mantidos), especialmente, como se seus testes tivessem essas três propriedades, então seu código também as possui.

Andy Lowry
fonte
11
+1 para uma lista abrangente destinada a unidade de teste (não integração ou teste funcional)
Gary Rowe
11
+1 para o link. Material interessante para ser encontrado lá.
Joris Meys
11
"Corra rapidamente" tem grandes implicações. É uma das razões pelas quais os testes de unidade devem ser executados isoladamente, longe de recursos externos, como banco de dados, sistema de arquivos, serviço da web etc. Isso, por sua vez, leva a zombarias / stubs.
9995 Michael Easter
11
quando diz It should run at the push of a button, isso significa que um teste de unidade não deve exigir contêineres (servidor de aplicativos) em execução (para a unidade que está sendo testada) ou uma conexão de recursos (como banco de dados, serviços da web externos etc.)? Estou confuso sobre quais partes de um aplicativo devem ser testadas em unidade e quais não. Foi-me dito que os testes de unidade não deveriam depender da conexão com o banco de dados e dos contêineres em execução, e talvez usassem maquetes.
amphibient
42

Um bom teste de unidade não reflete a função que está testando.

Como um exemplo bastante simplificado, considere que você tem uma função que retorna uma média de dois int. O teste mais abrangente chamaria a função e verificaria se um resultado é de fato uma média. Isso não faz nenhum sentido: você está espelhando (replicando) a funcionalidade que está testando. Se você cometeu um erro na função principal, cometerá o mesmo erro no teste.

Em outras palavras, se você se replicar na funcionalidade principal do teste de unidade, é um sinal provável de que está desperdiçando seu tempo.

mojuba
fonte
21
+1 O que você faria neste caso é testar com argumentos codificados e comparar com sua resposta conhecida.
Michael K
Eu já vi esse cheiro antes.
Paul Butcher
Você poderia dar um exemplo de um bom teste de unidade para a função que retorna médias?
VLAS
2
@VLAS testam valores predefinidos, por exemplo, certifique-se de que avg (1, 3) == 2 também verifique casos mais importantes, como INT_MAX, zeros, valores negativos, etc. Se um bug foi encontrado e corrigido na função, adicione outro teste para garantir que esse bug nunca seja reintroduzido.
Mjuba # 4/15
Interessante. Como você propõe obter as respostas corretas para essas entradas de teste e não cometer o mesmo erro do código submetido ao teste?
Timo
10

Bons testes de unidade são essencialmente a especificação na forma executável:

  1. descrever o comportamento do código correspondente aos casos de uso
  2. cobrir casos técnicos de canto (o que acontece se nulo for aprovado) - se um teste não estiver presente para um caso de canto, o comportamento será indefinido.
  3. quebrar se o código testado se afastar da especificação

Eu descobri que o desenvolvimento orientado a testes é muito adequado para rotinas de bibliotecas, pois você essencialmente escreve a API primeiro e depois a implementação real.


fonte
7

para TDD, "bom" testa os recursos de teste que o cliente deseja ; os recursos não correspondem necessariamente às funções e os cenários de teste não devem ser criados pelo desenvolvedor no vácuo

no seu caso - suponho - o 'recurso' é que a função de ajuste modela os dados de entrada dentro de uma certa tolerância a erros. Como não tenho ideia do que você realmente está fazendo, estou inventando algo; espero que seja análogo.

Exemplo de história:

Como um [X-Wing Pilot], eu quero [não mais que 0,0001% de erro de ajuste] para que [o computador de destino possa atingir a porta de escape da Estrela da Morte ao se mover a toda velocidade por um canyon]

Então você vai falar com os pilotos (e com o computador alvo, se for sensível). Primeiro você fala sobre o que é 'normal', depois fala sobre o anormal. Você descobre o que realmente importa nesse cenário, o que é comum, o que é improvável e o que é apenas possível.

Digamos que normalmente você terá uma janela de meio segundo em sete canais de dados de telemetria: velocidade, inclinação, rotação, guinada, vetor de destino, tamanho de destino e velocidade de destino, e esses valores serão constantes ou mudarão linearmente. De forma anormal, você pode ter menos canais e / ou os valores podem estar mudando rapidamente. Então, juntos, você faz alguns testes, como:

//Scenario 1 - can you hit the side of a barn?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is zero
    and all other values are constant,
Then:
    the error coefficient must be zero

//Scenario 2 - can you hit a turtle?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is less than c
    and all other values are constant,
Then:
    the error coefficient must be less than 0.0000000001/ns

...

//Scenario 42 - death blossom
Given:
    all 7 channels with 30% dropout and a 0.05 second sampling window
When:
    speed is zero
    and position is within enemy cluster
    and all targets are stationary
Then:
    the error coefficient must be less than 0.000001/ns for each target

Agora, você deve ter notado que não há cenário para a situação específica descrita na história. Acontece que, depois de conversar com o cliente e outras partes interessadas, esse objetivo na história original era apenas um exemplo hipotético. Os testes reais saíram da discussão que se seguiu. Isso pode acontecer. A história deve ser reescrita, mas não precisa ser [porque a história é apenas um espaço reservado para uma conversa com o cliente].

Steven A. Lowe
fonte
5

Crie testes para casos de canto, como um conjunto de testes contendo apenas o número mínimo de entradas (possível 1 ou 0) e alguns casos padrão. Esses testes de unidade não substituem os testes de aceitação completos, nem deveriam ser.

user281377
fonte
5

Eu já vi muitos casos em que as pessoas investem uma enorme quantidade de esforço escrevendo testes para códigos raramente inseridos, e não escrevendo testes para códigos inseridos com frequência.

Antes de se sentar para escrever qualquer teste, você deve observar algum tipo de gráfico de chamada, para garantir uma cobertura adequada.

Além disso, não acredito em escrever testes apenas para dizer "Sim, nós testamos isso". Se eu estiver usando uma biblioteca que é descartada e permanecerá imutável, não vou perder um dia escrevendo testes para garantir que as entranhas de uma API que nunca mudem funcionem conforme o esperado, mesmo que determinadas partes dela alto em um gráfico de chamada. Testes que consomem a referida biblioteca (meu próprio código) apontam isso.

Tim Post
fonte
mas o que mais tarde quando a biblioteca tiver uma versão mais recente com uma correção de bug?
@ Thorbjørn Ravn Andersen - Depende da biblioteca, o que mudou e seu próprio processo de teste. Não vou escrever testes para o código que sei que funciona quando o soltei e nunca o toquei. Portanto, se funcionar após a atualização, esqueça: :) É claro que há exceções.
Tim Post
se depender de sua biblioteca, a menos que você pode fazer é escrever testes que mostram o que você espera que a referida biblioteca para realmente fazer ,
... e se isso mudou, testes em coisas que consomem a referida biblioteca ... dr; Não preciso testar as entranhas do código de terceiros. Resposta atualizada para maior clareza, no entanto.
Tim Post
4

Não é tão TDD, mas depois de entrar no controle de qualidade, você pode melhorar seus testes configurando casos de teste para reproduzir os erros que surgirem durante o processo de controle de qualidade. Isso pode ser particularmente valioso quando você busca um suporte de longo prazo e começa a chegar a um local em que corre o risco de pessoas inadvertidamente reintroduzirem erros antigos. Ter um teste para capturar isso é particularmente valioso.

glenatron
fonte
3

Eu tento fazer com que todos os testes testem apenas uma coisa. Eu tento dar a cada teste um nome como shouldDoSomething (). Eu tento testar o comportamento, não a implementação. Eu só testei métodos públicos.

Normalmente, tenho um ou alguns testes de sucesso e, em seguida, talvez alguns testes de falha, por método público.

Eu uso muito maquetes. Uma boa estrutura de simulação provavelmente seria bastante útil, como o PowerMock. Embora eu não esteja usando ainda.

Se a classe A usar outra classe B, eu adicionaria uma interface, X, para que A não use B diretamente. Depois, criava o XMockup de mock-up e o usava em vez de B nos meus testes. Isso realmente ajuda a acelerar a execução do teste, reduzindo a complexidade do teste e também reduz o número de testes que eu escrevo para A, pois não preciso lidar com as peculiaridades de B. Posso, por exemplo, testar se A chama X.someMethod (). em vez de um efeito colateral de chamar B.someMethod ().

Mantenha seu código de teste limpo também.

Ao usar uma API, como uma camada de banco de dados, eu a simulava e permitia que a simulação gerasse uma exceção em todas as oportunidades possíveis sob comando. Em seguida, executo os testes um sem lançar e o em loop, sempre lançando uma exceção na próxima oportunidade até que o teste seja novamente bem-sucedido. Um pouco como os testes de memória disponíveis para o Symbian.

Roger CS Wernersson
fonte
2

Vejo que Andry Lowry já publicou as métricas de teste de unidade de Roy Osherove; mas parece que ninguém apresentou o conjunto (complementar) que o tio Bob fornece em Clean Code (132-133). Ele usa a sigla PRIMEIRO (aqui com meus resumos):

  • Rápido (eles devem correr rapidamente, para que as pessoas não se importem de executá-los)
  • Independente (os testes não devem configurar nem desmontar um para o outro)
  • Repetível (deve ser executado em todos os ambientes / plataformas)
  • Auto-validação (totalmente automatizada; a saída deve ser "aprovada" ou "falha", não um arquivo de log)
  • Oportuna (quando escrevê-las - antes de escrever o código de produção que eles testam)
Kazark
fonte