Como corrigir um erro no teste, depois de escrever a implementação

21

Qual é o melhor curso de ação no TDD se, depois de implementar a lógica corretamente, o teste ainda falhar (porque há um erro no teste)?

Por exemplo, suponha que você gostaria de desenvolver a seguinte função:

int add(int a, int b) {
    return a + b;
}

Suponha que o desenvolvamos nas seguintes etapas:

  1. Teste de gravação (nenhuma função ainda):

    // test1
    Assert.assertEquals(5, add(2, 3));
    

    Resultados em erro de compilação.

  2. Escreva uma implementação de função fictícia:

    int add(int a, int b) {
        return 5;
    }
    

    Resultado: test1passa.

  3. Adicione outro caso de teste:

    // test2 -- notice the wrong expected value (should be 11)!
    Assert.assertEquals(12, add(5, 6));
    

    Resultado: test2falha, test1ainda passa.

  4. Escreva implementação real:

    int add(int a, int b) {
        return a + b;
    }
    

    Resultado: test1ainda passa, test2ainda falha (desde 11 != 12).

Nesse caso em particular: seria melhor:

  1. correto test2e veja que agora passa ou
  2. exclua a nova parte da implementação (ou seja, volte para a etapa 2 acima), corrija test2e deixe falhar e, em seguida, reintroduza a implementação correta (etapa 4, acima).

Ou existe alguma outra maneira mais inteligente?

Embora eu entenda que o problema de exemplo é bastante trivial, estou interessado no que fazer no caso genérico, que pode ser mais complexo do que a adição de dois números.

EDIT (Em resposta à resposta de @Thomas Junk):

O foco desta pergunta é o que o TDD sugere nesse caso, e não a "melhor prática universal" para obter bons códigos ou testes (que podem ser diferentes do modo TDD).

Attilio
fonte
3
Refatorar contra a barra vermelha é um conceito relevante.
precisa saber é o seguinte
5
Claramente, você precisa fazer TDD no seu TDD.
Blrfl
17
Se alguém me perguntar por que sou cético em relação ao TDD, vou apontá-los para essa pergunta. Isso é kafkiano.
Traubenfuchs
@Blrfl Isso é o que nos diz Xibit »Coloquei o TDD em TDD para que você possa TDD enquanto TDDing«: D
Thomas Junk
3
@Traubenfuchs Admito que a pergunta parece boba à primeira vista e não sou defensora de "fazer TDD o tempo todo", mas acredito que há um grande benefício em ver um teste falhar e depois escrever um código que faça o teste passar (afinal, é sobre o que é essa pergunta).
Vincent Savard

Respostas:

31

O absolutamente crítico é que você veja o teste passar e falhar.

Se você exclui o código para fazer com que o teste falhe, reescreva-o ou espreite-o para a área de transferência apenas para colá-lo mais tarde, não importa. O TDD nunca disse que era preciso redigitar nada. Ele quer saber que o teste passa apenas quando deveria passar e falha somente quando deveria falhar.

Ver o teste passar e falhar é como você o testará. Nunca confie em um teste que nunca viu fazer as duas coisas.


A refatoração contra a barra vermelha fornece etapas formais para refatorar um teste de trabalho:

  • Execute o teste
    • Observe a barra verde
    • Quebrar o código que está sendo testado
  • Execute o teste
    • Observe a barra vermelha
    • Refatorar o teste
  • Execute o teste
    • Observe a barra vermelha
    • Descompacte o código que está sendo testado
  • Execute o teste
    • Observe a barra verde

No entanto, não estamos refatorando um teste de trabalho. Temos que transformar um teste de buggy. Uma preocupação é o código que foi introduzido enquanto apenas este teste o abordava. Esse código deve ser revertido e reintroduzido após a correção do teste.

Se não for esse o caso, e a cobertura do código não for uma preocupação devido a outros testes que cobrem o código, você pode transformar o teste e apresentá-lo como um teste verde.

Aqui, o código também está sendo revertido, mas apenas o suficiente para causar falha no teste. Se isso não for suficiente para cobrir todo o código introduzido, enquanto apenas coberto pelo teste de buggy, precisamos de uma reversão maior do código e de mais testes.

Introduzir um teste verde

  • Execute o teste
    • Observe a barra verde
    • Quebrar o código que está sendo testado
  • Execute o teste
    • Observe a barra vermelha
    • Descompacte o código que está sendo testado
  • Execute o teste
    • Observe a barra verde

Quebrar o código pode ser comentar o código ou movê-lo para outro lugar apenas para colá-lo novamente mais tarde. Isso nos mostra o escopo do código que o teste cobre.

Nas duas últimas corridas, você volta ao ciclo verde vermelho normal. Você está apenas colando em vez de digitar para descompactar o código e fazer o teste passar. Portanto, certifique-se de colar apenas o suficiente para fazer o teste passar.

O padrão geral aqui é ver a cor do teste mudar da maneira que esperamos. Observe que isso cria uma situação em que você faz um teste ecológico não confiável brevemente. Tenha cuidado para ser interrompido e esquecer onde você está nessas etapas.

Meus agradecimentos a RubberDuck pelo link Embracing the Red Bar .

candied_orange
fonte
2
Eu gosto mais desta resposta: é importante ver o teste falhar com código incorreto, para excluir / comentar o código, corrigir os testes e vê-los falhar, colocar de volta o código (talvez introduza um erro deliberado para colocar os testes no teste) e corrija o código para fazê-lo funcionar. É muito XP excluí-lo e reescrevê-lo completamente, mas às vezes você só precisa ser pragmático. ;)
GolezTrol
@GolezTrol Acho que minha resposta diz a mesma coisa, por isso gostaria de receber qualquer feedback que você tenha sobre se isso não está claro.
precisa saber é o seguinte
@jonrsharpe Sua resposta também é boa, e eu a votei antes mesmo de ler esta. Mas onde você é muito rigoroso ao reverter o código, o CandiedOrange sugere uma abordagem mais pragmática que me agrada mais.
precisa saber é o seguinte
@GolezTrol Eu não disse como reverter o código; comente, corte e cole, esconda, use o histórico do seu IDE; isso realmente não importa. O importante é por que você faz isso: para poder verificar se está obtendo o fracasso certo . Eu editei, espero esclarecer.
precisa saber é o seguinte
10

Qual é o objetivo geral que você deseja alcançar?

  • Fazendo bons testes?

  • Fazendo a implementação correta ?

  • Fazendo TTD religiosamente certo ?

  • Nenhuma das acima?

Talvez você pense demais em seu relacionamento com testes e testes.

Os testes não garantem a correção de uma implementação. Ter todos os testes aprovados não diz nada sobre se o seu software faz o que deveria; não faz declarações essencialistas sobre o seu software.

Tomando o seu exemplo:

A implementação "correta" da adição seria o código equivalente a a+b. E enquanto o seu código fizer isso, você diria que o algoritmo está correto no que faz e está implementado corretamente .

int add(int a, int b) {
    return a + b;
}

À primeira vista , nós dois concordamos que esta é a implementação de uma adição.

Mas o que estamos fazendo realmente não está dizendo, que este código é a implementação de additionque apenas comporta até certo ponto como uma: pensar de estouro de inteiro .

O excesso de número inteiro acontece no código, mas não no conceito de addition. Então: seu código se comporta até certo ponto como o conceito de addition, mas não é addition.

Este ponto de vista bastante filosófico tem várias consequências.

E uma é, você poderia dizer, que os testes nada mais são do que suposições do comportamento esperado do seu código. Ao testar seu código, você pode (talvez) nunca ter certeza de que sua implementação está correta , o melhor que você poderia dizer é que suas expectativas sobre os resultados que seu código fornece foram ou não atendidas; seja, que seu código esteja errado, seja, que seu teste esteja errado ou seja, que ambos estejam errados.

Testes úteis ajudam você a fixar suas expectativas sobre o que o código deve fazer: desde que eu não mude minhas expectativas e desde que o código modificado me dê o resultado esperado, posso ter certeza de que as suposições que fiz sobre os resultados parecem dar certo.

Isso não ajuda quando você fez as suposições erradas; mas ei! pelo menos evita a esquizofrenia: esperando resultados diferentes quando não deveria haver nenhum.


tl; dr

Qual é o melhor curso de ação no TDD se, depois de implementar a lógica corretamente, o teste ainda falhar (porque há um erro no teste)?

Seus testes são suposições sobre o comportamento do código. Se você tiver boas razões para pensar que sua implementação está correta, corrija o teste e verifique se essa suposição é válida.

Thomas Junk
fonte
1
Acho que a pergunta sobre os objetivos gerais é muito importante, obrigado por abordá-lo. Para mim, o maior prêmio é o seguinte: 1. implementação correta 2. testes "agradáveis" (ou, eu prefiro dizer, testes "úteis" / "bem projetados"). Eu vejo o TDD como uma ferramenta possível para alcançar esses dois objetivos. Portanto, embora eu não queira necessariamente seguir religiosamente o TDD, no contexto desta pergunta, estou mais interessado na perspectiva do TDD. Vou editar a pergunta para esclarecer isso.
Attilio 31/01
Então, você escreveria um teste que testa o estouro e passa quando isso acontece ou faria com que falhe porque acontece porque o algoritmo é adição e o excesso produz a resposta errada?
Jerry Jeremiah
1
@JerryJeremiah Meu argumento é: o que seus testes devem cobrir depende do seu caso de uso. Para um caso de uso em que você soma vários dígitos, o algoritmo é bom o suficiente . Se você sabe que é muito provável que você adicione "grandes números", datatypeé claramente a escolha errada. Um teste revelaria que: sua expectativa seria "funciona para grandes números" e, em muitos casos, não é atendida. Então a questão seria como lidar com esses casos. Eles são casos de canto? Quando sim, como lidar com eles? Talvez algumas cláusulas de pensão ajudem a evitar uma bagunça maior. A resposta é vinculada ao contexto.
Thomas Junk
7

Você precisa saber que o teste falhará se a implementação estiver errada, o que não é o mesmo que passar se a implementação estiver correta. Portanto, você deve colocar o código novamente em um estado em que espera que falhe antes de corrigir o teste e verifique se ele falhou pelo motivo esperado (ie 5 != 12), em vez de algo que não previu.

jonrsharpe
fonte
Como podemos verificar se o teste está falhando pelo motivo que esperamos?
Basilevs 29/01
2
@Basilevs você: 1. faça uma hipótese sobre qual deve ser o motivo da falha; 2. execute o teste; e 3. leia a mensagem de falha resultante e compare. Às vezes, isso também sugere maneiras de reescrever o teste para gerar um erro mais significativo (por exemplo, assertTrue(5 == add(2, 3))fornece uma saída menos útil do que assertEqual(5, add(2, 3))mesmo que ambos estejam testando a mesma coisa).
precisa saber é o seguinte
Ainda não está claro como aplicar esse princípio aqui. Eu tenho uma hipótese - teste retorna um valor constante, como executar o mesmo teste novamente garantiria que eu estivesse certo? Obviamente, para testar isso, preciso de mais um teste. Sugiro adicionar um exemplo explícito para responder.
Basilevs 29/01
1
@Basilevs what? Sua hipótese aqui na etapa 3 seria "o teste falha porque 5 não é igual a 12" . A execução do teste mostrará se o teste falhará por esse motivo, caso em que você prossegue ou por algum outro motivo, caso em que você descobrirá o motivo. Talvez este seja um problema de linguagem, mas não está claro para mim o que você está sugerindo.
precisa saber é o seguinte
5

Nesse caso em particular, se você alterar o 12 para o 11 e o teste agora passar, acho que você fez um bom trabalho ao testar o teste e a implementação, para que não haja muita necessidade de passar por outras etapas.

No entanto, o mesmo problema pode surgir em situações mais complexas, como quando você tem um erro no seu código de instalação. Nesse caso, depois de corrigir seu teste, você provavelmente deve tentar alterar sua implementação de forma a fazer com que esse teste específico falhe e depois reverter a mutação. Se a reversão da implementação for a maneira mais fácil de fazer isso, tudo bem. No seu exemplo, você pode mudar a + bpara a + aou a * b.

Como alternativa, se você pode alterar a asserção levemente e ver o teste falhar, isso pode ser bastante eficaz no teste do teste.

Vaughn Cato
fonte
0

Eu diria que esse é o caso do seu sistema de controle de versão favorito:

  1. Faça a correção do teste, mantendo as alterações de código no diretório de trabalho.
    Confirme com uma mensagem correspondente Fixed test ... to expect correct output.

    Com git, isso pode exigir o uso de git add -pse teste e implementação estiverem no mesmo arquivo; caso contrário, você poderá obviamente apenas preparar os dois arquivos separadamente.

  2. Confirme o código de implementação.

  3. Volte no tempo para testar a confirmação feita na etapa 1, certificando-se de que o teste realmente falhe .

Veja bem, dessa maneira, você não confia nas suas proezas de edição para mover seu código de implementação para fora do caminho enquanto você testa seu teste com falha. Você emprega seu VCS para salvar seu trabalho e garantir que o histórico gravado do VCS inclua corretamente o teste de reprovação e aprovação.

cmaster - restabelece monica
fonte