Teste vs Não se repita (DRY)


11

Por que se repetir escrevendo testes tão altamente encorajados?

Parece que os testes expressam basicamente a mesma coisa que o código e, portanto, é uma duplicata (no conceito, não na implementação) do código. O objetivo final do DRY não incluiria a eliminação de todo o código de teste?

Respostas:


24

Eu acredito que isso é um equívoco de qualquer maneira que eu possa pensar.

O código de teste que testa o código de produção não é nada parecido. Vou demonstrar em python:

def multiply(a, b):
    """Multiply ``a`` by ``b``"""
    return a*b

Então, um teste simples seria:

def test_multiply():
    assert multiply(4, 5) == 20

Ambas as funções têm uma definição semelhante, mas ambas fazem coisas muito diferentes. Nenhum código duplicado aqui. ;-)

Também ocorre que as pessoas escrevem testes duplicados tendo essencialmente uma asserção por função de teste. Isso é loucura e já vi pessoas fazendo isso. Isso é uma má prática.

def test_multiply_1_and_3():
    """Assert that a multiplication of 1 and 3 is 3."""
    assert multiply(1, 3) == 3

def test_multiply_1_and_7():
    """Assert that a multiplication of 1 and 7 is 7."""
    assert multiply(1, 7) == 7

def test_multiply_3_and_4():
    """Assert that a multiplication of 3 and 4 is 12."""
    assert multiply(3, 4) == 12

Imagine fazer isso para mais de 1000 linhas de código eficazes. Em vez disso, você testa por 'recurso':

def test_multiply_positive():
    """Assert that positive numbers can be multiplied."""
    assert multiply(1, 3) == 3
    assert multiply(1, 7) == 7
    assert multiply(3, 4) == 12

def test_multiply_negative():
    """Assert that negative numbers can be multiplied."""
    assert multiply(1, -3) == -3
    assert multiply(-1, -7) == 7
    assert multiply(-3, 4) == -12

Agora, quando os recursos são adicionados / removidos, só tenho que considerar adicionar / remover uma função de teste.

Você deve ter notado que eu não apliquei forloops. Isso ocorre porque repetir algumas coisas é bom. Quando eu aplicaria loops, o código seria muito menor. Mas quando uma afirmação falha, pode ofuscar a saída que exibe uma mensagem ambígua. Se isso ocorrer, em seguida, os testes serão menos útil e você vai precisar de um depurador para inspecionar onde as coisas dão errado.


8
Uma afirmação por teste é tecnicamente recomendada, pois significa que vários problemas não aparecerão como uma única falha. No entanto, na prática, acho que a agregação cuidadosa de asserções reduz a quantidade de código repetido e quase nunca adiro a uma afirmação por diretriz de teste.
Rob Church

@ pink-diamond-square vejo que o NUnit não para de testar depois que uma afirmação falha (o que eu acho estranho). Nesse caso específico, é realmente melhor ter uma afirmação por teste. Se uma estrutura de teste de unidade interromper o teste após uma falha na asserção, várias asserções são melhores.
siebz0r

3
O NUnit não para todo o conjunto de testes, mas esse teste é interrompido, a menos que você tome medidas para evitá-lo (você pode capturar a exceção que gera, o que é útil ocasionalmente). O ponto que acho que eles estão argumentando é que, se você escrever testes que incluem mais de uma afirmação, não obterá todas as informações necessárias para corrigir o problema. Para trabalhar com seu exemplo, imagine que essa função de multiplicação não goste do número 3. Nesse caso, assert multiply(1,3)falharia, mas você também não obteria o relatório de teste com falha assert multiply(3,4).
Rob Church

Eu apenas pensei em aumentá-lo porque uma única afirmação por teste é, pelo que li no mundo .net, a "boa prática" e múltiplas afirmações é "uso pragmático". Parece um pouco diferente na documentação do Python, onde o exemplo def test_shuffleexecuta duas declarações.
Rob Church

Concordo e discordo: D Há claramente repetição aqui: assert multiply(*, *) == *para que você possa definir uma assert_multiplyfunção. No cenário atual, isso não importa pela contagem de linhas e pela legibilidade, mas por testes mais longos, você pode reutilizar asserções complicadas, acessórios, código de geração de acessórios, etc. Não sei se essa é uma prática recomendada, mas geralmente faço esta.
Inf3rno

10

Parece que os testes expressam basicamente a mesma coisa que o código e, portanto, é uma duplicata

Não, isto não é verdade.

Os testes têm uma finalidade diferente da sua implementação:

  • Os testes garantem que sua implementação funcione.
  • Eles servem como documentação: Examinando os testes, você vê os contratos que seu código deve cumprir, ou seja, qual entrada retorna qual saída, quais são os casos especiais etc.
  • Além disso, seus testes garantem que, à medida que você adiciona novos recursos, a funcionalidade existente não falha.

4

Não. DRY é sobre escrever código apenas uma vez para executar uma tarefa específica. Testes são a validação de que a tarefa está sendo executada corretamente. É um pouco semelhante a um algoritmo de votação, onde obviamente usar o mesmo código seria inútil.


2

O objetivo final do DRY não incluiria a eliminação de todo o código de teste?

Não, o objetivo final do DRY realmente significaria a eliminação de todo o código de produção .

Se nossos testes pudessem ser especificações perfeitas do que queremos que o sistema faça, teríamos de gerar automaticamente o código de produção correspondente (ou binários) automaticamente, removendo efetivamente a base de código de produção em si.

Na verdade, é isso que abordagens como a arquitetura orientada a modelos pretendem alcançar - uma única fonte de verdade projetada pelo homem a partir da qual tudo é derivado pela computação.

Eu não acho que o contrário (se livrar de todos os testes) seja desejável porque:

  • Você precisa resolver a incompatibilidade de impedância entre implementação e especificação. O código de produção pode transmitir a intenção até certo ponto, mas nunca será tão fácil raciocinar sobre testes bem expressos. Nós, seres humanos, precisamos de uma visão mais ampla do porquê estamos construindo coisas. Mesmo que você não faça testes por causa do DRY, as especificações provavelmente terão que ser anotadas nos documentos de qualquer maneira, o que é definitivamente uma fera mais perigosa em termos de incompatibilidade de impedância e dessincronização de código, se você me perguntar.
  • Embora o código de produção possa ser facilmente derivado de especificações executáveis ​​corretas (assumindo tempo suficiente), um conjunto de testes é muito mais difícil de reconstituir a partir do código final de um programa. As especificações não aparecem claramente apenas olhando o código, porque é difícil perceber as interações entre as unidades de código em tempo de execução. É por isso que temos tanta dificuldade em lidar com aplicativos herdados sem teste. Em outras palavras: se você deseja que seu aplicativo sobreviva por mais de alguns meses, provavelmente será melhor perder o disco rígido que hospeda sua base de código de produção do que aquele em que está sua suíte de testes.
  • É muito mais fácil introduzir um erro acidentalmente no código de produção do que no código de teste. E como o código de produção não é auto-verificável (embora isso possa ser abordado com o Design by Contract ou com sistemas mais avançados), ainda precisamos de algum programa externo para testá-lo e nos avisar se ocorrer uma regressão.

1

Porque às vezes se repetir é bom. Nenhum desses princípios deve ser adotado em todas as circunstâncias, sem dúvida ou contexto. Às vezes, escrevi testes contra uma versão ingênua (e lenta) de um algoritmo, que é uma violação bastante clara do DRY, mas definitivamente benéfica.


1

Desde teste de unidade é sobre como fazer alterações não intencionais mais difícil, às vezes pode fazer mudanças intencionais mais difícil, também. Este fato está realmente relacionado ao princípio DRY.

Por exemplo, se você tiver uma função MyFunctionchamada no código de produção em apenas um local e escrever 20 testes de unidade, poderá facilmente ter 21 lugares no código onde essa função é chamada. Agora, quando você precisa alterar a assinatura de MyFunction, ou a semântica, ou ambos (porque alguns requisitos mudam), você tem 21 lugares para mudar em vez de apenas um. E o motivo é realmente uma violação do princípio DRY: você repetiu (pelo menos) a mesma chamada de função MyFunction21 vezes.

A abordagem correta para esse caso é a aplicação do princípio DRY ao seu código de teste: ao escrever 20 testes de unidade, encapsule as chamadas MyFunctionnos testes de unidade em apenas algumas funções auxiliares (idealmente, apenas uma), usadas pelo 20 testes de unidade. Idealmente, você acaba com apenas dois lugares na sua chamada de código MyFunction: um do seu código de produção e um dos seus testes de unidade. Portanto, quando você precisar alterar a assinatura MyFunctionposteriormente, terá apenas alguns lugares para alterar em seus testes.

"Alguns lugares" ainda são mais do que "um lugar" (o que você obtém sem testes de unidade), mas as vantagens de ter testes de unidade devem superar em muito a vantagem de ter menos código para alterar (caso contrário, você fará testes de unidade completamente errado).


0

Um dos maiores desafios para a construção de software é capturar requisitos; ou seja, para responder à pergunta "o que esse software deve fazer?" O software precisa de requisitos exatos para definir com precisão o que o sistema precisa fazer, mas aqueles que definem as necessidades de sistemas e projetos de software geralmente incluem pessoas que não possuem um software ou formação formal (matemática). A falta de rigor na definição de requisitos forçou o desenvolvimento de software a encontrar uma maneira de validar o software para os requisitos.

A equipe de desenvolvimento se viu traduzindo a descrição coloquial de um projeto em requisitos mais rigorosos. A disciplina de testes se uniu como ponto de verificação para o desenvolvimento de software, para preencher a lacuna entre o que o cliente diz que quer e o que o software entende que ele quer. Tanto os desenvolvedores de software quanto a equipe de qualidade / teste formam o entendimento da especificação (informal) e cada (independentemente) escreve software ou testes para garantir que seu entendimento esteja em conformidade. A inclusão de outra pessoa para entender os requisitos (imprecisos) adicionou perguntas e perspectivas diferentes para aprimorar ainda mais a precisão dos requisitos.

Como sempre houve testes de aceitação, era natural expandir a função de teste para escrever testes automatizados e de unidade. O problema era que isso significava contratar programadores para fazer testes e, assim, você reduzia a perspectiva da garantia de qualidade para os programadores que realizavam testes.

Dito isso, você provavelmente está fazendo testes errados se os testes diferem pouco dos programas reais. A sugestão de Msdy seria focar mais o que nos testes e menos como.

A ironia é que, em vez de capturar uma especificação formal de requisitos a partir da descrição coloquial, a indústria optou por implementar testes de pontos como código para automatizar os testes. Em vez de produzir requisitos formais aos quais o software pode ser construído para responder, a abordagem adotada foi testar alguns pontos, em vez de abordar a criação de software usando lógica formal. Este é um compromisso, mas tem sido bastante eficaz e relativamente bem-sucedido.


0

Se você acha que seu código de teste é muito semelhante ao código de implementação, isso pode ser uma indicação de que você está usando demais uma estrutura de simulação. O teste baseado em simulação em um nível muito baixo pode terminar com a configuração de teste semelhante ao método que está sendo testado. Tente escrever testes de nível superior com menor probabilidade de interrupção se você alterar sua implementação (eu sei que isso pode ser difícil, mas se você puder gerenciá-lo, terá um conjunto de testes mais útil como resultado).


0

Os testes de unidade não devem incluir uma duplicação do código em teste, como já foi observado.

Eu acrescentaria, no entanto, que os testes de unidade normalmente não são tão SECOS quanto o código de "produção", porque a instalação tende a ser semelhante (mas não idêntica) nos testes ... especialmente se você tiver um número significativo de dependências que você está zombando / fingindo.
É claro que é possível refatorar esse tipo de coisa em um método de instalação comum (ou conjunto de métodos de instalação) ... mas descobri que esses métodos de instalação tendem a ter longas listas de parâmetros e são bastante frágeis.

Então seja pragmático. Se você pode consolidar o código de instalação sem comprometer a manutenção, faça isso de qualquer maneira. Mas se a alternativa for um conjunto complexo e frágil de métodos de configuração, um pouco de repetição nos seus métodos de teste está OK.

Um evangelista local do TDD / BDD coloca desta maneira:
"Seu código de produção deve estar SECO. Mas não há problema em seus testes serem 'úmidos'".


0

Parece que os testes expressam basicamente a mesma coisa que o código e, portanto, é uma duplicata (no conceito, não na implementação) do código.

Isso não é verdade, os testes descrevem os casos de uso, enquanto o código descreve um algoritmo que passa nos casos de uso, o que é mais geral. Pelo TDD, você começa escrevendo casos de uso (provavelmente com base na história do usuário) e depois implementa o código necessário para passar esses casos de uso. Então você escreve um pequeno teste, um pequeno pedaço de código e depois refatora, se necessário, para se livrar das repetições. É assim que funciona.

Por testes, pode haver repetições também. Por exemplo, você pode reutilizar equipamentos, gerar códigos, declarações complicadas, etc. Normalmente, faço isso para evitar bugs nos testes, mas geralmente esqueço de testar primeiro se um teste realmente falha e pode realmente arruinar o dia. , quando você procura o bug no código por meia hora e o teste está errado ... xD

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.