O código duplicado é mais tolerável em testes de unidade?

113

Eu estraguei vários testes de unidade algum tempo atrás, quando os analisei e os refatorou para torná-los mais SECOS - a intenção de cada teste não estava mais clara. Parece que há uma compensação entre a capacidade de leitura e manutenção dos testes. Se eu deixar o código duplicado nos testes de unidade, eles serão mais legíveis, mas se eu alterar o SUT , terei que rastrear e alterar cada cópia do código duplicado.

Você concorda que existe essa compensação? Em caso afirmativo, você prefere que seus testes sejam legíveis ou fáceis de manter?

Daryl Spitzer
fonte

Respostas:

68

O código duplicado é um cheiro no código de teste de unidade tanto quanto em outro código. Se você tiver código duplicado nos testes, será mais difícil refatorar o código de implementação porque você tem um número desproporcional de testes para atualizar. Os testes devem ajudá-lo a refatorar com confiança, em vez de ser uma grande carga que impede seu trabalho no código que está sendo testado.

Se a duplicação estiver em uma configuração de fixação, considere fazer mais uso do setUpmétodo ou fornecer mais (ou mais flexíveis) Métodos de Criação .

Se a duplicação está no código que manipula o SUT, pergunte-se por que vários testes chamados de “unidade” estão exercendo exatamente a mesma funcionalidade.

Se a duplicação estiver nas afirmações, talvez você precise de algumas afirmações personalizadas . Por exemplo, se vários testes tiverem uma sequência de afirmações como:

assertEqual('Joe', person.getFirstName())
assertEqual('Bloggs', person.getLastName())
assertEqual(23, person.getAge())

Então, talvez você precise de um único assertPersonEqualmétodo para poder escrever assertPersonEqual(Person('Joe', 'Bloggs', 23), person). (Ou talvez você simplesmente precise sobrecarregar o operador de igualdade Person.)

Como você mencionou, é importante que o código de teste seja legível. Em particular, é importante que a intenção de um teste seja clara. Eu acho que se muitos testes parecem quase iguais (por exemplo, três quartos das linhas iguais ou virtualmente iguais) é difícil detectar e reconhecer as diferenças significativas sem ler e compará-las cuidadosamente. Portanto, acho que a refatoração para remover a duplicação ajuda na legibilidade, porque cada linha de cada método de teste é diretamente relevante para o propósito do teste. Isso é muito mais útil para o leitor do que uma combinação aleatória de linhas que são diretamente relevantes e linhas que são apenas clichês.

Dito isso, às vezes os testes exercem situações complexas que são semelhantes, mas ainda assim significativamente diferentes, e é difícil encontrar uma boa maneira de reduzir a duplicação. Use o bom senso: se você sentir que os testes são legíveis e deixar sua intenção clara, e se sentir confortável com a talvez necessidade de atualizar mais do que um número teoricamente mínimo de testes ao refatorar o código invocado pelos testes, aceite a imperfeição e mude para algo mais produtivo. Você sempre pode voltar e refatorar os testes mais tarde, quando a inspiração bater!

spiv
fonte
30
"Código duplicado é um cheiro no código de teste de unidade tanto quanto em outro código." Não. "Se você duplicou o código nos testes, fica mais difícil refatorar o código de implementação porque você tem um número desproporcional de testes para atualizar." Isso acontece porque você está testando a API privada em vez da API pública.
15
Mas para evitar código duplicado em testes de unidade, geralmente você precisa introduzir uma nova lógica. Não acho que os testes de unidade devam conter lógica, porque então você precisaria de testes de unidade de testes de unidade.
Petr Peller
@ user11617 defina "API privada" e "API pública". No meu entendimento, API pública é a API que é visível para o mundo externo / consumidores de terceiros e explicitamente com versão via SemVer ou similar, qualquer outra coisa é privada. Com esta definição, quase todos os testes de unidade estão testando "API privada" e, portanto, mais sensíveis à duplicação de código, o que eu acho que é verdade.
KolA
@KolA "Público" não significa consumidores terceirizados - esta não é uma API da web. A API pública de uma classe se refere aos métodos que devem ser usados ​​pelo código do cliente (que normalmente não / não deveria estar mudando tanto) - geralmente os métodos "públicos". A API privada se refere à lógica e aos métodos usados ​​internamente. Eles não devem ser acessados ​​de fora da classe. Esta é uma razão pela qual é importante encapsular corretamente a lógica em uma classe usando modificadores de acesso ou a convenção na linguagem que está sendo usada.
Nathan
@Nathan qualquer pacote library / dll / nuget tem consumidores de terceiros, não precisa ser uma API da web. O que mencionei é que é muito comum declarar classes públicas e membros que não devem ser usados ​​diretamente pelos consumidores da biblioteca (ou, na melhor das hipóteses, torná-los internos e fazer anotações em assembly com InternalsVisibleToAttribute) apenas para permitir que os testes de unidade os alcancem diretamente. Isso leva a uma série de testes acoplados à implementação e os torna mais um fardo do que uma vantagem
KolA
186

A legibilidade é mais importante para os testes. Se um teste falhar, você deseja que o problema seja óbvio. O desenvolvedor não deve ter que percorrer muitos códigos de teste altamente fatorados para determinar exatamente o que falhou. Você não quer que seu código de teste se torne tão complexo que precise escrever testes de unidade.

No entanto, eliminar a duplicação geralmente é uma coisa boa, desde que não obscureça nada, e eliminar a duplicação em seus testes pode levar a uma API melhor. Apenas certifique-se de não ultrapassar o ponto de retornos decrescentes.

Kristopher Johnson
fonte
xUnit e outros contêm um argumento de 'mensagem' em chamadas de declaração. É uma boa ideia colocar frases significativas para permitir que os desenvolvedores encontrem rapidamente os resultados dos testes que falharam.
fim de
1
@seand Você pode tentar explicar o que sua declaração está verificando, mas quando ela está falhando e contém código um tanto obscuro, o desenvolvedor precisará ir e desenrolá-lo de qualquer maneira. IMO É mais importante ter um código que se descreva lá.
IgorK,
1
@Kristopher,? Por que isso é postado como wiki da comunidade?
Pacerier
@Pacerier, não sei. Costumava haver regras complicadas sobre as coisas se tornarem automaticamente wiki da comunidade.
Kristopher Johnson
Pois a legibilidade dos relatórios é mais importante do que os testes, especialmente ao fazer integração ou teste de ponta a ponta, os cenários podem ser complexos o suficiente para evitar navegar um pouquinho, é normal encontrar a falha, mas novamente para mim a falha nos relatórios deve explicar o problema suficientemente bem.
Anirudh
47

O código de implementação e os testes são animais diferentes e as regras de fatoração se aplicam de maneira diferente a eles.

Código ou estrutura duplicados são sempre um cheiro no código de implementação. Quando você começa a ter um padrão em implementação, precisa revisar suas abstrações.

Por outro lado, o código de teste deve manter um nível de duplicação. A duplicação no código de teste atinge dois objetivos:

  • Manter os testes desacoplados. O acoplamento de teste excessivo pode dificultar a alteração de um único teste com falha que precisa ser atualizado porque o contrato foi alterado.
  • Manter os testes significativos de forma isolada. Quando um único teste está falhando, deve ser razoavelmente simples descobrir exatamente o que ele está testando.

Eu tendo a ignorar a duplicação trivial no código de teste, desde que cada método de teste permaneça menor que cerca de 20 linhas. Gosto quando o ritmo de configuração-execução-verificação é aparente nos métodos de teste.

Quando a duplicação surge na parte "verificar" dos testes, costuma ser benéfico definir métodos de asserção personalizados. Claro, esses métodos ainda devem testar uma relação claramente identificada que pode ser tornada aparente no nome do método: assertPegFitsInHole-> bom, assertPegIsGood-> ruim.

Quando os métodos de teste se tornam longos e repetitivos, às vezes acho útil definir modelos de teste para preencher as lacunas que usam alguns parâmetros. Em seguida, os métodos de teste reais são reduzidos a uma chamada ao método de modelo com os parâmetros apropriados.

Quanto a muitas coisas em programação e teste, não há uma resposta clara. Você precisa desenvolver um gosto, e a melhor maneira de fazer isso é cometendo erros.

ddaa
fonte
8

Concordo. A troca existe, mas é diferente em lugares diferentes.

É mais provável que eu refatore código duplicado para configurar o estado. Mas é menos provável que refatore a parte do teste que realmente exercita o código. Dito isso, se exercitar o código sempre exige várias linhas de código, então posso pensar que é um cheiro e refatorar o código real em teste. E isso vai melhorar a legibilidade e a manutenção do código e dos testes.

sino de estuque
fonte
Eu acho que isso é uma boa idéia. Se você tiver muita duplicação, veja se pode refatorar para criar um "dispositivo de teste" comum sob o qual muitos testes podem ser executados. Isso eliminará o código de configuração / desmontagem duplicado.
Programador Outlaw
8

Você pode reduzir a repetição usando vários tipos diferentes de métodos de utilitário de teste .

Sou mais tolerante com a repetição no código de teste do que no código de produção, mas às vezes fico frustrado com isso. Quando você altera o design de uma classe e tem que voltar e ajustar 10 métodos de teste diferentes que executam as mesmas etapas de configuração, é frustrante.

Don Kirkby
fonte
6

Jay Campos cunhou a frase que "DSLs deve ser úmido, não seca", onde DAMP meios descritiva e frases significativas . Acho que o mesmo se aplica aos testes também. Obviamente, muita duplicação é ruim. Mas remover a duplicação a todo custo é ainda pior. Os testes devem atuar como especificações reveladoras de intenção. Se, por exemplo, você especificar o mesmo recurso de vários ângulos diferentes, uma certa quantidade de duplicação é esperada.

Jörg W Mittag
fonte
3

EU AMO rspec por causa disso:

Tem 2 coisas para ajudar -

  • grupos de exemplos compartilhados para testar o comportamento comum.
    você pode definir um conjunto de testes e então 'incluir' esse conjunto em seus testes reais.

  • contextos aninhados.
    você pode essencialmente ter um método de 'configuração' e 'desmontagem' para um subconjunto específico de seus testes, não apenas para cada um na classe.

Quanto mais cedo .NET / Java / outras estruturas de teste adotarem esses métodos, melhor (ou você pode usar IronRuby ou JRuby para escrever seus testes, que eu pessoalmente acho que é a melhor opção)

Orion Edwards
fonte
3

Acho que o código de teste requer um nível semelhante de engenharia que normalmente seria aplicado ao código de produção. Certamente pode haver argumentos a favor da legibilidade e eu concordaria que isso é importante.

Em minha experiência, entretanto, acho que testes bem fatorados são mais fáceis de ler e entender. Se houver 5 testes em que cada um parece o mesmo, exceto para uma variável que foi alterada e a afirmação no final, pode ser muito difícil encontrar o que é esse único item diferente. Da mesma forma, se for fatorado de forma que apenas a variável que está mudando seja visível e a asserção, será fácil descobrir o que o teste está fazendo imediatamente.

Encontrar o nível certo de abstração durante o teste pode ser difícil e acho que vale a pena fazer.

Kevin London
fonte
2

Não acho que haja uma relação entre código mais duplicado e legível. Acho que seu código de teste deve ser tão bom quanto o outro código. O código não repetitivo é mais legível do que o código duplicado quando bem feito.

Paco
fonte
2

Idealmente, os testes de unidade não devem mudar muito uma vez que são escritos, então eu tenderia para a legibilidade.

Ter os testes de unidade tão discretos quanto possível também ajuda a manter os testes focados na funcionalidade específica que eles têm como objetivo.

Dito isso, minha tendência é tentar e reutilizar certas partes do código que acabo usando continuamente, como o código de configuração que é exatamente o mesmo em um conjunto de testes.

17 de 26
fonte
2

"os refatorou para torná-los mais SECOS - a intenção de cada teste não era mais clara"

Parece que você teve problemas para refatorar. Estou apenas supondo, mas se acabou menos claro, isso não significa que você ainda tem mais trabalho a fazer para que você tenha testes razoavelmente elegantes que são perfeitamente claros?

É por isso que os testes são uma subclasse de UnitTest - para que você possa criar bons conjuntos de testes que sejam corretos, fáceis de validar e claros.

Antigamente, tínhamos ferramentas de teste que usavam diferentes linguagens de programação. Era difícil (ou impossível) criar testes agradáveis ​​e fáceis de trabalhar.

Você tem todo o poder de - qualquer linguagem que estiver usando - Python, Java, C # - então use bem essa linguagem. Você pode obter um código de teste de boa aparência, claro e não muito redundante. Não há compensação.

S.Lott
fonte