O que faz um bom teste unitário?

Tenho certeza que a maioria de vocês está escrevendo muitos testes automatizados e que você também se deparou com algumas armadilhas comuns quando testou a unidade.

Minha pergunta é: você segue alguma regra de conduta para escrever testes para evitar problemas no futuro? Para ser mais específico: Quais são as propriedades dos bons testes unitários ou como você escreve seus testes?

Sugestões agnósticas de idiomas são incentivadas.

Deixe-me começar por ligar fonts – Pragmatic Unit Testing em Java com JUnit (Há uma versão com C # -Nunit também .. mas eu tenho este um .. é agnóstico para a maior parte. Recomendado.)

Bons testes devem ser A TRIP (A sigla não é pegajosa o suficiente – eu tenho uma impressão da folha de cheats no livro que eu tive que tirar para ter certeza de que entendi direito ..)

  • Automático : a chamada de testes, bem como a verificação dos resultados para PASSA / FALHA, devem ser automáticas
  • Completo : Cobertura; Embora os bugs tendam a se agrupar em torno de determinadas regiões no código, assegure-se de testar todos os principais caminhos e cenários. Use ferramentas se precisar conhecer regiões não testadas
  • Repetível : os testes devem produzir os mesmos resultados sempre .. sempre. Os testes não devem depender de parâmetros incontroláveis.
  • Independente : muito importante.
    • Os testes devem testar apenas uma coisa de cada vez. Múltiplas asserções estão bem, desde que todas estejam testando um recurso / comportamento. Quando um teste falha, ele deve identificar a localização do problema.
    • Os testes não devem confiar um no outro – Isolados. Nenhuma suposição sobre a ordem de execução do teste. Assegure-se de “limpar a barreira” antes de cada teste usando a configuração / desassembly apropriadamente
  • Profissional : A longo prazo, você terá tanto código de teste quanto produção (se não mais), portanto, siga o mesmo padrão de bom design para seu código de teste. Métodos bem fundamentados – classs com nomes reveladores de intenção, sem duplicação, testes com bons nomes, etc.

  • Bons testes também são executados rapidamente . qualquer teste que leve mais de meio segundo para ser executado precisa ser trabalhado. Quanto mais tempo o conjunto de testes leva para uma execução, menos frequentemente ele será executado. Quanto mais mudanças o desenvolvedor tentar fazer entre as execuções … se alguma coisa quebrar … levará mais tempo para descobrir qual mudança foi o culpado.

Atualização 2010-08:

  • Legível : Isso pode ser considerado parte do Profissional – no entanto, não pode ser enfatizado o suficiente. Um teste ácido seria encontrar alguém que não faz parte de sua equipe e pedir a ele que descubra o comportamento em teste dentro de alguns minutos. Os testes precisam ser mantidos exatamente como o código de produção – para facilitar a leitura, mesmo que seja necessário mais esforço. Os testes devem ser simétricos (seguir um padrão) e concisos (testar um comportamento por vez). Use uma convenção de nomenclatura consistente (por exemplo, o estilo TestDox). Evite atravancar o teste com “detalhes incidentais” … torne-se um minimalista.

Além destes, a maioria dos outros são diretrizes que reduzem o trabalho de baixo benefício: por exemplo, ‘Não teste código que você não possui’ (por exemplo, DLLs de terceiros). Não faça testes de getters e setters. Fique de olho na relação custo-benefício ou na probabilidade de defeito.

  1. Não escreva testes gigantescos. Como a ‘unidade’ em ‘teste de unidade’ sugere, torne cada um tão atômico e isolado quanto possível. Se você precisar, crie condições prévias usando objects simulados, em vez de recriar muito do ambiente típico do usuário manualmente.
  2. Não teste coisas que obviamente funcionam. Evite testar as classs de um fornecedor de terceiros, especialmente aquele que fornece as principais APIs da estrutura na qual você codifica. Por exemplo, não teste a adição de um item à class Hashtable do fornecedor.
  3. Considere o uso de uma ferramenta de cobertura de código , como NCover, para ajudar a descobrir os casos de borda que você ainda precisa testar.
  4. Tente escrever o teste antes da implementação. Pense no teste como mais uma especificação que sua implementação irá aderir. Cf. também desenvolvimento orientado a comportamento, um ramo mais específico do desenvolvimento orientado a testes.
  5. Ser consistente. Se você só escreve testes para algum do seu código, isso é pouco útil. Se você trabalha em equipe, e alguns ou todos os outros não escrevem testes, também não é muito útil. Convença a si mesmo e a todos os outros sobre a importância (e as propriedades que poupam tempo ) de testar ou não se incomode.

A maioria das respostas aqui parece abordar as melhores práticas de teste de unidade em geral (quando, onde, por que e o quê), em vez de realmente escrever os testes em si (como). Como a pergunta parecia bem específica sobre a parte “como”, pensei em postar isso, tirado de uma apresentação de “bolsa marrom” que fiz na minha empresa.

5 Leis de Escrita de Womp:


1. Use nomes longos e descritivos de methods de teste.

- Map_DefaultConstructorShouldCreateEmptyGisMap() - ShouldAlwaysDelegateXMLCorrectlyToTheCustomHandlers() - Dog_Object_Should_Eat_Homework_Object_When_Hungry() 

2. Escreva seus testes em um estilo Arrange / Act / Assert .

  • Embora essa estratégia organizacional tenha existido por algum tempo e tenha chamado muitas coisas, a introdução da sigla “AAA” recentemente tem sido uma ótima maneira de fazer isso acontecer. Tornar todos os seus testes consistentes com o estilo AAA torna-os fáceis de ler e manter.

3. Sempre forneça uma mensagem de falha com suas afirmações.

 Assert.That(x == 2 && y == 2, "An incorrect number of begin/end element processing events was raised by the XElementSerializer"); 
  • Uma prática simples, mas recompensadora, que torna óbvio em seu aplicativo de corredor o que falhou. Se você não fornecer uma mensagem, normalmente obterá algo como “Esperado, verdadeiro, falso” na saída de falha, o que faz com que você realmente precise ler o teste para descobrir o que está errado.

4. Comente o motivo do teste – qual é a suposição de negócios?

  /// A layer cannot be constructed with a null gisLayer, as every function /// in the Layer class assumes that a valid gisLayer is present. [Test] public void ShouldNotAllowConstructionWithANullGisLayer() { } 
  • Isso pode parecer óbvio, mas essa prática protegerá a integridade de seus testes de pessoas que não entendem a razão por trás do teste. Eu vi muitos testes serem removidos ou modificados que estavam perfeitamente bem, simplesmente porque a pessoa não entendia as suposições que o teste estava verificando.
  • Se o teste é trivial ou o nome do método é suficientemente descritivo, pode ser permitido deixar o comentário desativado.

5. Todo teste deve sempre reverter o estado de qualquer recurso que toque

  • Use mocks sempre que possível para evitar lidar com resources reais.
  • A limpeza deve ser feita no nível de teste. Os testes não devem ter qualquer confiança na ordem de execução.

Mantenha estes objectives em mente (adaptado do livro xUnit Test Patterns by Meszaros)

  • Os testes devem reduzir o risco, não introduzi-lo.
  • Os testes devem ser fáceis de executar.
  • Os testes devem ser fáceis de manter à medida que o sistema evolui em torno deles

Algumas coisas para tornar isso mais fácil:

  • Os testes devem falhar apenas por um motivo.
  • Testes só devem testar uma coisa
  • Minimize as dependencies de teste (sem dependencies em bancos de dados, arquivos, interface do usuário etc.)

Não esqueça que você também pode fazer testes de integração com sua estrutura xUnit, mas manter testes de integração e testes de unidade separados

Os testes devem ser isolados. Um teste não deve depender de outro. Ainda mais, um teste não deve depender de sistemas externos. Em outras palavras, teste seu código, não o código do qual seu código depende.Você pode testar essas interações como parte de sua integração ou testes funcionais.

Algumas propriedades de grandes testes unitários:

  • Quando um teste falha, deve ser imediatamente óbvio onde está o problema. Se você precisar usar o depurador para rastrear o problema, seus testes não serão granulares o suficiente. Ter exatamente uma afirmação por teste ajuda aqui.

  • Quando você refatorar, nenhum teste deve falhar.

  • Os testes devem correr tão rápido que você nunca hesite em executá-los.

  • Todos os testes devem passar sempre; não há resultados não determinísticos.

  • Os testes de unidade devem ser bem fatorados, assim como o seu código de produção.

@Alotor: Se você está sugerindo que uma biblioteca só deve ter testes de unidade em sua API externa, eu discordo. Eu quero testes de unidade para cada class, incluindo classs que eu não exponho para chamadores externos. (No entanto, se eu sentir a necessidade de escrever testes para methods privados, então preciso refatorar ) .


EDIT: Houve um comentário sobre duplicação causada por “uma afirmação por teste”. Especificamente, se você tiver algum código para configurar um cenário e, em seguida, desejar fazer várias asserções sobre ele, mas tiver apenas uma asserção por teste, poderá duplicar a configuração em vários testes.

Eu não entendo essa abordagem. Em vez disso, eu uso fixtures de teste por cenário . Aqui está um exemplo aproximado:

 [TestFixture] public class StackTests { [TestFixture] public class EmptyTests { Stack _stack; [TestSetup] public void TestSetup() { _stack = new Stack(); } [TestMethod] [ExpectedException (typeof(Exception))] public void PopFails() { _stack.Pop(); } [TestMethod] public void IsEmpty() { Assert(_stack.IsEmpty()); } } [TestFixture] public class PushedOneTests { Stack _stack; [TestSetup] public void TestSetup() { _stack = new Stack(); _stack.Push(7); } // Tests for one item on the stack... } } 

O que você está procurando é o delineamento dos comportamentos da class em teste.

  1. Verificação de comportamentos esperados.
  2. Verificação de casos de erro.
  3. Cobertura de todos os caminhos de código dentro da class.
  4. Exercitando todas as funções de membro dentro da class.

A intenção básica é aumentar sua confiança no comportamento da class.

Isso é especialmente útil ao procurar refatorar seu código. Martin Fowler tem um artigo interessante sobre testes em seu site.

HTH.

Felicidades,

Roubar

O teste deve originalmente falhar. Então você deve escrever o código que os faz passar, caso contrário, você corre o risco de escrever um teste que está com defeito e sempre passa.

Eu gosto do acrônimo BICEP da direita do livro Pragmatic Unit Testing acima mencionado:

  • Certo : os resultados estão corretos ?
  • B : Todas as condições secundárias estão corretas?
  • I : Podemos verificar relações anversas?
  • C : Podemos checar os resultados usando outros meios?
  • E : Podemos forçar as condições de erro a acontecerem?
  • P : As características do desempenho estão dentro dos limites?

Pessoalmente eu sinto que você pode ir muito longe, verificando se você obtém os resultados corretos (1 + 1 deve retornar 2 em uma function de adição), experimentando todas as condições de contorno que você pode imaginar (como usar dois números é maior que o valor máximo inteiro na function add) e forçando condições de erro, como falhas de rede.

Bons testes precisam ser sustentáveis.

Eu ainda não descobri como fazer isso para ambientes complexos.

Todos os livros didáticos começam a ficar descolados à medida que sua base de código começa a chegar às centenas de milhares ou milhões de linhas de código.

  • Interações entre equipes explodem
  • número de casos de teste explodem
  • interações entre componentes explodem.
  • tempo para construir todos os unittests torna-se uma parte significativa do tempo de construção
  • uma alteração de API pode repercutir em centenas de casos de teste. Mesmo que a mudança do código de produção tenha sido fácil.
  • o número de events necessários para sequenciar processos no estado correto aumenta, o que aumenta o tempo de execução do teste.

Uma boa arquitetura pode controlar parte da explosão da interação, mas, inevitavelmente, à medida que os sistemas se tornam mais complexos, o sistema de testes automatizados cresce com ela.

É aqui que você começa a lidar com trocas:

  • somente teste API externa, caso contrário refatoração internals resulta em retrabalho significativo no caso de teste.
  • a configuração e desassembly de cada teste fica mais complicada à medida que um subsistema encapsulado retém mais estado.
  • a compilation noturna e a execução automatizada de testes aumentam para horas.
  • tempos de compilation e execução aumentados significa que os projetistas não executam ou não executarão todos os testes
  • para reduzir os tempos de execução de teste que você considera os testes de sequenciamento a serem reduzidos para configurar e desmontar

Você também precisa decidir:

onde você armazena casos de teste em sua base de código?

  • Como você documenta seus casos de teste?
  • Os equipamentos de teste podem ser reutilizados para salvar a manutenção do caso de teste?
  • o que acontece quando uma execução de caso de teste noturno falha? Quem faz a triagem?
  • Como você mantém os objects simulados? Se você tiver 20 módulos, todos usando seu próprio sabor de uma API de log de simulação, alterando as ondulações da API rapidamente. Não apenas os casos de teste mudam, mas os 20 objects simulados mudam. Esses 20 módulos foram escritos durante vários anos por muitas equipes diferentes. É um problema clássico de reutilização.
  • os indivíduos e suas equipes entendem o valor dos testes automatizados, eles simplesmente não gostam de como a outra equipe está fazendo isso. 🙂

Eu poderia continuar para sempre, mas meu ponto é que:

Os testes precisam ser sustentáveis.

Cobri esses princípios há algum tempo neste artigo da MSDN Magazine, que considero importante para qualquer desenvolvedor ler.

A maneira que eu defino “bons” testes unitários é se eles possuem as três propriedades a seguir:

  • Eles são legíveis (nomeando, afirma, variables, comprimento, complexidade ..)
  • Eles são mantidos (sem lógica, não mais especificada, baseada em estado, refatorada ..)
  • Eles são dignos de confiança (teste a coisa certa, isolada, não testes de integração ..)
  • O Teste de Unidade apenas testa a API externa da sua Unidade, você não deve testar o comportamento interno.
  • Cada teste de um TestCase deve testar um (e apenas um) método dentro desta API.
    • Casos de Teste Adicionais devem ser incluídos para casos de falha.
  • Teste a cobertura de seus testes: Uma vez testada uma unidade, 100% das linhas dentro desta unidade devem ter sido executadas.

Jay Fields tem muitos bons conselhos sobre como escrever testes unitários e há um post onde ele resume os conselhos mais importantes . Lá você vai ler que você deve pensar criticamente sobre o seu contexto e julgar se o conselho vale a pena para você. Você recebe uma tonelada de respostas incríveis aqui, mas decide se é melhor para o seu contexto. Experimente-os e refatorando apenas se cheirar mal a você.

Atenciosamente

Nunca assuma que um método trivial de 2 linhas funcionará. Escrever um teste de unidade rápido é a única maneira de evitar que o teste nulo ausente, o sinal de menos e o erro de escopo sutil de morder você, inevitavelmente, quando você tiver menos tempo para lidar com ele do que agora.

Eu segundo a resposta “A TRIP”, exceto que os testes DEVEM confiar um no outro !!!

Por quê?

DRY – Don’t Repeat Yourself – aplica-se também aos testes! As dependencies de teste podem ajudar a 1) salvar o tempo de configuração, 2) salvar resources do dispositivo elétrico e 3) identificar falhas. Naturalmente, somente dado que sua estrutura de teste suporta dependencies de primeira class. Caso contrário, admito que são ruins.

Acompanhamento http://www.iam.unibe.ch/~scg/Research/JExample/

Frequentemente, os testes unitários são baseados em objects falsos ou dados simulados. Eu gosto de escrever três tipos de testes unitários:

  • testes de unidade “transientes”: eles criam seus próprios objects / dados simulados e testam sua function com eles, mas destroem tudo e não deixam rastros (como nenhum dado em um database de teste)
  • teste de unidade “persistente”: eles testam funções dentro de seu código criando objects / dados que serão necessários posteriormente por uma function mais avançada para seu próprio teste de unidade (evitando que essas funções avançadas recriem cada vez que seu conjunto de objects / dados simulados)
  • testes unitários “baseados em persistência”: testes unitários usando objects / dados simulados que já estão lá (porque foram criados em outra session de teste de unidade) pelos testes de unidade persistentes.

O objective é evitar a repetição de tudo para poder testar todas as funções.

  • Eu corro o terceiro tipo com muita frequência porque todos os objects / dados falsos já estão lá.
  • Eu corro o segundo tipo sempre que o meu modelo muda.
  • Eu corro o primeiro para verificar as funções básicas de vez em quando, para verificar regressões básicas.

Pense nos dois tipos de testes e trate-os de forma diferente – testes funcionais e testes de desempenho.

Use inputs e métricas diferentes para cada um. Você pode precisar usar software diferente para cada tipo de teste.

Eu uso uma convenção de nomenclatura de teste consistente descrita por padrões de nomenclatura de unidade de teste de Roy Osherove Cada método em uma determinada class de caso de teste tem o seguinte estilo de nomeação MethodUnderTest_Scenario_ExpectedResult.

    A primeira seção do nome do teste é o nome do método no sistema em teste.
    Em seguida é o cenário específico que está sendo testado.
    Finalmente, os resultados desse cenário.

Cada seção usa o Upper Camel Case e é delimitada por uma pontuação inferior.

Eu achei isso útil quando eu executo o teste que o teste é agrupado pelo nome do método em teste. E ter uma convenção permite que outros desenvolvedores entendam a intenção do teste.

Eu também acrescentar parâmetros ao nome do método se o método em teste tiver sido sobrecarregado.