Ao fazer TDD e escrever um teste de unidade, como resistir ao desejo de "trapacear" ao escrever a primeira iteração do código de "implementação" que você está testando?
Por exemplo:
Vamos precisar calcular o fatorial de um número. Começo com um teste de unidade (usando o MSTest), algo como:
[TestClass]
public class CalculateFactorialTests
{
[TestMethod]
public void CalculateFactorial_5_input_returns_120()
{
// Arrange
var myMath = new MyMath();
// Act
long output = myMath.CalculateFactorial(5);
// Assert
Assert.AreEqual(120, output);
}
}
Eu executo esse código e ele falha, pois o CalculateFactorial
método nem existe. Portanto, agora escrevo a primeira iteração do código para implementar o método em teste, escrevendo o código mínimo necessário para passar no teste.
O problema é que sou continuamente tentado a escrever o seguinte:
public class MyMath
{
public long CalculateFactorial(long input)
{
return 120;
}
}
Tecnicamente, isso é correto, na medida em que realmente é o código mínimo necessário para fazer esse teste específico passar (verde), embora seja claramente uma "fraude", pois nem sequer tenta executar a função de calcular um fatorial. Obviamente, agora a parte da refatoração se torna um exercício para "escrever a funcionalidade correta" em vez de uma refatoração verdadeira da implementação. Obviamente, a adição de testes adicionais com parâmetros diferentes falhará e forçará uma refatoração, mas você deve começar com esse teste.
Então, minha pergunta é: como você consegue esse equilíbrio entre "escrever o código mínimo para passar no teste" enquanto ainda o mantém funcional e no espírito do que você está realmente tentando alcançar?
fonte
Respostas:
É perfeitamente legítimo. Vermelho, Verde, Refatorador.
O primeiro teste passa.
Adicione o segundo teste, com uma nova entrada.
Agora, quando estiver verde rapidamente, você poderá adicionar um if-else, que funciona bem. Passa, mas você ainda não terminou.
A terceira parte do Red, Green, Refactor é a mais importante. Refatorar para remover a duplicação . Você terá duplicação no seu código agora. Duas instruções retornando números inteiros. E a única maneira de remover essa duplicação é codificar a função corretamente.
Não estou dizendo que não escreva corretamente da primeira vez. Só estou dizendo que não é trapaça, se não o fizer.
fonte
Claramente, é necessária uma compreensão do objetivo final e a obtenção de um algoritmo que atenda esse objetivo.
TDD não é uma bala mágica para design; você ainda precisa saber como resolver problemas usando o código e ainda precisa fazer isso em um nível superior a algumas linhas de código para fazer um teste ser aprovado.
Eu gosto da ideia do TDD, porque incentiva um bom design; isso faz você pensar em como escrever seu código para que ele possa ser testado e, geralmente, essa filosofia empurrará o código para um design melhor em geral. Mas você ainda precisa saber como arquitetar uma solução.
Não sou a favor de filosofias reducionistas de TDD que afirmam que você pode aumentar um aplicativo simplesmente escrevendo a menor quantidade de código para passar no teste. Sem pensar em arquitetura, isso não funcionará, e seu exemplo prova isso.
Tio Bob Martin diz o seguinte:
fonte
Uma pergunta muito boa ... e eu tenho que discordar de quase todo mundo, exceto o @Robert.
Escrita
para uma função fatorial fazer uma prova passar é uma perda de tempo . Não é "trapaça", nem segue literalmente o refator vermelho-verde. Está errado .
Aqui está o porquê:
os argumentos de 'refatoração' são equivocados; se você tem dois casos de teste para 5 e 6, este código ainda é errado, porque você não está calculando um fatorial em tudo :
se seguirmos literalmente o argumento 'refatorar' , quando tivermos 5 casos de teste, chamaremos o YAGNI e implementaremos a função usando uma tabela de pesquisa:
Nenhum destes são realmente calcular nada, você é . E essa não é a tarefa!
fonte
Quando você escreve apenas um teste de unidade, a implementação de uma linha (
return 120;
) é legítima. Escrever um loop calculando o valor de 120 - isso seria trapaça!Tais testes iniciais simples são uma boa maneira de capturar casos extremos e evitar erros pontuais. Cinco realmente não é o valor de entrada com o qual eu começaria.
Uma regra prática que pode ser útil aqui é: zero, um, muitos, lotes . Zero e um são casos importantes para o fatorial. Eles podem ser implementados com one-liners. O "muitos" casos de teste (por exemplo, 5!) Forçariam você a escrever um loop. O caso de teste "lotes" (1000 !?) pode forçar você a implementar um algoritmo alternativo para lidar com números muito grandes.
fonte
factorial(5)
é um primeiro teste ruim. começamos com os casos mais simples possíveis e, em cada iteração, tornamos os testes um pouco mais específicos, exigindo que o código se torne um pouco mais genérico. é isso que o tio bob chama de premissa de prioridade de transformação ( blog.8thlight.com/uncle-bob/2013/05/27/… )Contanto que você tenha apenas um único teste, o código mínimo necessário para passar no teste é realmente verdadeiro
return 120;
, e você pode mantê-lo facilmente enquanto não tiver mais testes.Isso permite que você adie o design adicional até escrever os testes que exercem OUTROS valores de retorno desse método.
Lembre-se de que o teste é a versão executável da sua especificação e, se tudo o que essa especificação diz é que f (6) = 120, isso se encaixa perfeitamente.
fonte
Se você conseguir "trapacear" dessa maneira, isso sugere que seus testes de unidade são defeituosos.
Em vez de testar o método fatorial com um único valor, teste-o como um intervalo de valores. O teste orientado a dados pode ajudar aqui.
Veja seus testes de unidade como uma manifestação dos requisitos - eles devem definir coletivamente o comportamento do método que eles testam. (Isso é conhecido como desenvolvimento orientado ao comportamento - é o futuro
;-)
)Então, pergunte a si mesmo - se alguém mudasse a implementação para algo incorreto, seus testes ainda passariam ou eles diriam "espere um minuto!"?
Tendo isso em mente, se o seu único teste foi o da sua pergunta, tecnicamente, a implementação correspondente está correta. O problema é então visto como requisitos mal definidos.
fonte
case
instruções aswitch
e não pode escrever um teste para todas as entradas e saídas possíveis para o exemplo do OP.Int64.MinValue
paraInt64.MaxValue
. Levaria muito tempo para ser executado, mas definiria explicitamente o requisito, sem espaço para erros. Com a tecnologia atual, isso é inviável (suspeito que possa se tornar mais comum no futuro) e concordo que você poderia trapacear, mas acho que a questão dos OPs não era prática (ninguém realmente trapacearia de tal maneira na prática), mas teórico.Basta escrever mais testes. Eventualmente, seria mais curto escrever
do que
:-)
fonte
Escrever testes de "fraude" é bom, para valores suficientemente pequenos de "OK". Mas lembre-se: o teste de unidade só é concluído quando todos os testes passam e nenhum teste novo pode ser gravado que irá falhar . Se você realmente deseja ter um método CalculateFactorial que contém várias instruções if (ou melhor ainda, uma declaração switch / case grande :-), você pode fazer isso e, desde que esteja lidando com um número de precisão fixa, o código necessário implementar isso é finito (embora provavelmente seja grande e feio, e talvez limitado por limitações do compilador ou do sistema no tamanho máximo do código de um procedimento). Neste ponto, se você realmenteinsista para que todo o desenvolvimento seja conduzido por um teste de unidade, você pode escrever um teste que exija o código para calcular o resultado em um período de tempo menor que o que pode ser realizado seguindo todas as ramificações da instrução if .
Basicamente, o TDD pode ajudá-lo a escrever código que implementa os requisitos corretamente , mas não pode forçá-lo a escrever um bom código. Isso é contigo.
Compartilhe e curta.
fonte
Eu concordo 100% com a sugestão de Robert Harveys aqui, não se trata apenas de fazer os testes passarem, é preciso manter o objetivo geral em mente também.
Como uma solução para o seu ponto de vista de "só é verificado para funcionar com um determinado conjunto de entradas", eu proporia o uso de testes orientados a dados, como a teoria da xunidade. O poder por trás desse conceito é que ele permite criar facilmente Especificações de entradas e saídas.
Para os fatoriais, um teste seria assim:
Você pode até implementar um fornecimento de dados de teste (que retorna
IEnumerable<Tuple<xxx>>
) e codificar uma invariante matemática, como dividir repetidamente por n resultará em n-1).Acho que essa tp é uma maneira muito poderosa de testar.
fonte
Se você ainda conseguir trapacear, os testes não serão suficientes. Escreva mais testes! Para o seu exemplo, tentarei adicionar testes com as entradas 1, -1, -1000, 0, 10, 200.
No entanto, se você realmente se comprometer a trapacear, pode escrever um interminável "se-então". Nesse caso, nada poderia ajudar, exceto a revisão de código. Você logo seria pego no teste de aceitação ( escrito por outra pessoa! )
O problema dos testes de unidade é que algumas vezes os programadores os consideram um trabalho desnecessário. A maneira correta de vê-los é como uma ferramenta para você corrigir o resultado do seu trabalho. Portanto, se você criar um if-then, saberá inconscientemente que existem outros casos a serem considerados. Isso significa que você deve escrever outros testes. E assim sucessivamente até você perceber que a trapaça não está funcionando e é melhor codificar da maneira correta. Se você ainda sente que não terminou, não terminou.
fonte
Eu sugeriria que sua escolha de teste não é a melhor.
Eu começaria com:
fatorial (1) como o primeiro teste,
fatorial (0) como o segundo
fatorial (-ve) como o terceiro
e depois continue com casos não triviais
e termine com um estojo de transbordamento.
fonte
-ve
??