Digamos que você esteja escrevendo um estilo TDD de jogo Yahtzee. Você deseja testar a parte do código que determina se um conjunto de cinco rolagens é ou não uma casa cheia. Tanto quanto eu sei, ao fazer TDD, você segue estes princípios:
- Escreva os testes primeiro
- Escreva a coisa mais simples possível que funcione
- Refinar e refatorar
Portanto, um teste inicial pode ser algo como isto:
public void Returns_true_when_roll_is_full_house()
{
FullHouseTester sut = new FullHouseTester();
var actual = sut.IsFullHouse(1, 1, 1, 2, 2);
Assert.IsTrue(actual);
}
Ao seguir a "Escreva a coisa mais simples possível que funcione", você deve agora escrever o IsFullHouse
método assim:
public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
{
return true;
}
return false;
}
Isso resulta em um teste verde, mas a implementação está incompleta.
Você deve testar todas as combinações válidas possíveis (valores e posições) para uma casa cheia? Essa é a única maneira de ter certeza absoluta de que seu IsFullHouse
código está completamente testado e correto, mas também parece bastante insano fazer isso.
Como você testaria unitariamente algo assim?
Atualizar
Erik e Kilian apontam que o uso de literais na implementação inicial para obter um teste verde pode não ser a melhor idéia. Eu gostaria de explicar por que fiz isso e essa explicação não se encaixa em um comentário.
Minha experiência prática com testes de unidade (especialmente usando uma abordagem TDD) é muito limitada. Lembro-me de assistir a uma gravação da TDD Masterclass de Roy Osherove no Tekpub. Em um dos episódios, ele constrói um estilo TDD da String Calculator. A especificação completa da Calculadora de cordas pode ser encontrada aqui: http://osherove.com/tdd-kata-1/
Ele começa com um teste como este:
public void Add_with_empty_string_should_return_zero()
{
StringCalculator sut = new StringCalculator();
int result = sut.Add("");
Assert.AreEqual(0, result);
}
Isso resulta nesta primeira implementação do Add
método:
public int Add(string input)
{
return 0;
}
Então este teste é adicionado:
public void Add_with_one_number_string_should_return_number()
{
StringCalculator sut = new StringCalculator();
int result = sut.Add("1");
Assert.AreEqual(1, result);
}
E o Add
método é refatorado:
public int Add(string input)
{
if (input.Length == 0)
{
return 0;
}
return 1;
}
Depois de cada passo, Roy diz "Escreva a coisa mais simples que funcionará".
Então, pensei em experimentar essa abordagem ao tentar fazer um jogo Yahtzee no estilo TDD.
fonte
if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
Respostas:
Já existem muitas boas respostas para essa pergunta, e eu comentei e votei em várias delas. Ainda assim, gostaria de acrescentar alguns pensamentos.
Flexibilidade não é para iniciantes
O OP afirma claramente que ele não tem experiência com TDD, e acho que uma boa resposta deve levar isso em conta. Na terminologia do modelo Dreyfus de aquisição de habilidades , ele provavelmente é um novato . Não há nada errado em ser um novato - somos todos novatos quando começamos a aprender algo novo. No entanto, o que o modelo Dreyfus explica é que os novatos são caracterizados por
Isso não é uma descrição de uma deficiência de personalidade, então não há razão para ter vergonha disso - é um estágio pelo qual todos precisamos passar para aprender algo novo.
Isso também se aplica ao TDD.
Embora eu concorde com muitas das outras respostas aqui de que o TDD não precisa ser dogmático e que às vezes pode ser mais benéfico trabalhar de uma maneira alternativa, isso não ajuda ninguém a começar. Como você pode exercer julgamento discricionário quando não tem experiência?
Se um novato aceita o conselho de que às vezes não há problema em fazer o TDD, como ele pode determinar quando não há problema em ignorar o TDD?
Sem experiência ou orientação, a única coisa que um iniciante pode fazer é pular o TDD toda vez que se tornar muito difícil. Essa é a natureza humana, mas não é uma boa maneira de aprender.
Ouça os testes
Ignorar o TDD sempre que se tornar difícil é perder um dos benefícios mais importantes do TDD. Os testes fornecem feedback antecipado sobre a API do SUT. Se o teste for difícil de escrever, é um sinal importante de que o SUT é difícil de usar.
Esta é a razão pela qual uma das mensagens mais importantes do GOOS é: ouça seus testes!
No caso desta pergunta, minha primeira reação ao ver a API proposta para o jogo Yahtzee e a discussão sobre combinatória que pode ser encontrada nesta página, foi que esse é um feedback importante sobre a API.
A API precisa representar rolos de dados como uma sequência ordenada de números inteiros? Para mim, aquele cheiro de obsessão primitiva . Foi por isso que fiquei feliz ao ver a resposta de tallseth sugerindo a introdução de uma
Roll
aula. Eu acho que é uma excelente sugestão.No entanto, acho que alguns dos comentários a essa resposta estão errados. O que o TDD sugere é que, uma vez que você tenha a ideia de que uma
Roll
classe seria uma boa ideia, suspenda o trabalho no SUT original e comece a trabalhar no TDD daRoll
classe.Embora eu concorde que o TDD seja mais voltado para o 'caminho feliz' do que para testes abrangentes, ainda ajuda a dividir o sistema em unidades gerenciáveis. Uma
Roll
aula soa como algo que você pode concluir com TDD com muito mais facilidade.Então, uma vez que a
Roll
classe tenha evoluído o suficiente, você retornaria ao SUT original e o detalharia em termos deRoll
entradas.A sugestão de um auxiliar de teste não implica necessariamente aleatoriedade - é apenas uma maneira de tornar o teste mais legível.
Outra maneira de abordar e modelar a entrada em termos de
Roll
instâncias seria a introdução de um Test Data Builder .Vermelho / Verde / Refatorador é um processo de três etapas
Embora eu concorde com o sentimento geral de que (se você é suficientemente experiente em TDD), não precisa seguir rigorosamente o TDD, acho que é um péssimo conselho no caso de um exercício de Yahtzee. Embora eu não conheça os detalhes das regras do Yahtzee, não vejo aqui um argumento convincente de que você não pode seguir rigorosamente o processo Vermelho / Verde / Refatorar e ainda assim obter um resultado adequado.
O que a maioria das pessoas aqui parece esquecer é o terceiro estágio do processo Vermelho / Verde / Refatorador. Primeiro você escreve o teste. Então você escreve a implementação mais simples que passa em todos os testes. Então você refatora.
É aqui, neste terceiro estado, que você pode levar todas as suas habilidades profissionais. É aqui que você pode refletir sobre o código.
No entanto, acho que é um absurdo afirmar que você deve apenas "Escrever a coisa mais simples possível que não seja completamente irracional e obviamente incorreta que funcione". Se você (pensa que conhece) o suficiente sobre a implementação de antemão, tudo o que estiver abaixo da solução completa ficará obviamente incorreto . No que diz respeito aos conselhos, então, isso é bastante inútil para um iniciante.
O que realmente deve acontecer é que, se você puder fazer todos os testes passarem com uma implementação obviamente incorreta , isso significa que você deve escrever outro teste .
É surpreendente a frequência com que isso o leva a uma implementação totalmente diferente daquela que você tinha em mente primeiro. Às vezes, a alternativa que cresce assim pode ser melhor do que o seu plano original.
Rigor é uma ferramenta de aprendizado
Faz muito sentido seguir processos rigorosos como Vermelho / Verde / Refatorar, desde que se esteja aprendendo. Isso força o aluno a ganhar experiência com TDD não apenas quando é fácil, mas também quando é difícil.
Somente quando você dominou todas as partes difíceis, você está em posição de tomar uma decisão informada sobre quando se desviar do caminho 'verdadeiro'. É quando você começa a formar seu próprio caminho.
fonte
Como um aviso, este é o TDD como eu o pratico e, como Kilian apropriadamente aponta, eu seria cauteloso com qualquer um que sugerisse que existe uma maneira correta de praticá-lo. Mas talvez isso ajude você ...
Primeiro de tudo, a coisa mais simples que você poderia fazer para passar no teste seria:
Isso é significativo porque não por causa de alguma prática de TDD, mas porque a codificação em todos esses literais não é realmente uma boa ideia. Uma das coisas mais difíceis de entender com o TDD é que não é uma estratégia de teste abrangente - é uma maneira de se proteger contra regressões e marcar o progresso, mantendo o código simples. É uma estratégia de desenvolvimento e não uma estratégia de teste.
A razão de eu mencionar essa distinção é que ela ajuda a orientar os testes que você deve escrever. A resposta para "que testes devo escrever?" é "quaisquer testes necessários para obter o código da maneira que você deseja". Pense no TDD como uma maneira de ajudá-lo a descobrir algoritmos e raciocinar sobre seu código. Portanto, considerando seu teste e minha implementação "simples verde", que teste vem a seguir? Bem, você estabeleceu algo que é uma casa cheia, então quando não é uma casa cheia?
Agora você precisa descobrir uma maneira de diferenciar os dois casos de teste que são significativos . Pessoalmente, eu acrescentaria um pouco de informações esclarecedoras para "fazer a coisa mais simples para fazer o teste passar" e dizer "fazer a coisa mais simples para fazer o teste passar que favorece sua implementação". Escrever testes com falha é o seu pretexto para alterar o código; portanto, quando você escreve cada teste, você deve se perguntar "o que meu código não faz e que eu quero que faça e como posso expor essa deficiência?" Também pode ajudá-lo a tornar seu código robusto e a lidar com casos extremos. O que você faz se um chamador insere disparates?
Resumindo, se você estiver testando todas as combinações de valores, certamente estará fazendo algo errado (e provavelmente acabará com uma explosão combinatória de condicionais). Quando se trata de TDD, você deve escrever a quantidade mínima de casos de teste necessários para obter o algoritmo desejado. Quaisquer outros testes que você escrever começarão a ser ecológicos e, portanto, tornar-se-ão documentação, em essência, e não estritamente parte do processo TDD. Você só escreverá outros casos de teste TDD se os requisitos forem alterados ou se um bug for exposto; nesse caso, você documentará a deficiência com um teste e depois passará.
Atualizar:
Comecei isso como um comentário em resposta à sua atualização, mas ela começou a ficar bastante longa ...
Eu diria que o problema não está na existência de literais, ponto final, mas com a coisa 'mais simples' sendo uma condicional em 5 partes. Quando você pensa sobre isso, uma condicional de 5 partes é realmente bastante complicada. Será comum usar literais durante a etapa de vermelho para verde e depois abstraí-los para constantes na etapa de refatoração ou generalizá-las em um teste posterior.
Durante minha própria jornada com o TDD, percebi que há uma distinção importante a ser feita - não é bom confundir "simples" e "obtuso". Ou seja, quando eu comecei, vi as pessoas fazendo TDD e pensei "eles estão fazendo a coisa mais burra possível para fazer os testes passarem" e eu imitei isso por um tempo, até perceber que "simples" era sutilmente diferente do que "obtuso". Às vezes eles se sobrepõem, mas muitas vezes não.
Então, desculpas se eu desse a impressão de que a existência de literais era o problema - não é. Eu diria que a complexidade do condicional com as 5 cláusulas é o problema. Seu primeiro vermelho para verde pode ser apenas "retorno verdadeiro", porque isso é realmente simples (e obtuso, por coincidência). O próximo caso de teste, com o (1, 2, 3, 4, 5), terá que retornar falso, e é aqui que você começa a deixar "obtuso" para trás. Você deve se perguntar "por que (1, 1, 1, 2, 2) é uma casa cheia e (1, 2, 3, 4, 5) não é?" A coisa mais simples que você poderia sugerir seria que um tivesse o último elemento de sequência 5 ou o segundo elemento de sequência 2 e o outro não. Essas são simples, mas também são (desnecessariamente) obtusas. O que você realmente quer dirigir é "quantos do mesmo número eles têm?" Portanto, você pode obter a aprovação do segundo teste, verificando se há ou não uma repetição. No que se repete, você tem uma casa cheia e no outro, não. Agora o teste passa e você escreve outro caso de teste que tem uma repetição, mas não é uma casa completa para refinar ainda mais seu algoritmo.
Você pode ou não fazer isso com literais à medida que avança, e tudo bem se você fizer. Mas a idéia geral é aumentar seu algoritmo 'organicamente' à medida que você adiciona mais casos.
fonte
Testar cinco valores literais específicos em uma combinação específica não é "mais simples" para o meu cérebro febril. Se a solução para um problema é realmente óbvia (conte se você tem exatamente três e exatamente dois de qualquer valor), siga em frente e codifique essa solução e escreva alguns testes que seriam muito, muito improváveis de satisfazer acidentalmente com a quantidade de código que você escreveu (isto é, literais diferentes e ordens diferentes dos triplos e duplos).
As máximas do TDD são realmente ferramentas, não crenças religiosas. O objetivo deles é fazer com que você escreva códigos corretos e bem fatorados rapidamente. Se uma máxima obviamente estiver no caminho disso, pule para a frente e vá para o próximo passo. Haverá muitos bits não óbvios em seu projeto, onde você poderá aplicá-lo.
fonte
A resposta de Erik é ótima, mas achei que poderia compartilhar um truque na escrita de testes.
Comece com este teste:
Esse teste fica ainda melhor se você criar uma
Roll
classe em vez de passar 5 parâmetros:Isso fornece essa implementação:
Em seguida, escreva este teste:
Quando isso passar, escreva este:
Depois disso, aposto que você não precisa mais escrever (talvez dois pares, ou talvez yahtzee, se você acha que não é uma casa cheia).
Obviamente, implemente seus métodos Any para retornar Rolls aleatórios que atendam às suas críticas.
Existem alguns benefícios nessa abordagem:
fonte
IsFullHouse
Realmente deveria retornartrue
sepairNum == trioNum
?Posso pensar em duas maneiras principais que consideraria ao testar isso;
Adicione "mais" alguns casos de teste (~ 5) de conjuntos válidos de casa cheia e a mesma quantidade de falsas esperadas ({1, 1, 2, 3, 3} é boa. Lembre-se de que 5 casos, por exemplo, podem ser reconhecido como "3 do mesmo mais um par" por uma implementação incorreta). Esse método pressupõe que o desenvolvedor não esteja apenas tentando passar nos testes, mas implementando-o corretamente.
Teste todos os conjuntos possíveis de dados (existem apenas 252 diferentes). É claro que isso pressupõe que você tem alguma maneira de saber qual é a resposta esperada (ao testar isso é conhecido como um
oracle
.) Essa poderia ser uma implementação de referência da mesma função ou de um ser humano. Se você quer ser realmente rigoroso, pode valer a pena codificar manualmente cada resultado esperado.Por acaso, escrevi uma IA do Yahtzee uma vez, que obviamente tinha que conhecer as regras. Você pode encontrar o código para a parte de avaliação de pontuação aqui . Observe que a implementação é para a versão escandinava (Yatzy) e nossa implementação supõe que os dados são dados em ordem classificada.
fonte
Este exemplo realmente erra o alvo. Estamos falando de uma única função direta aqui, não de um design de software. Isso é um pouco complicado? sim, então você divide. E você absolutamente não testa todas as entradas possíveis de 1, 1, 1, 1, 1 a 6, 6, 6, 6, 6, 6. A função em questão não requer ordem, apenas uma combinação, ou seja, AAABB.
Você não precisa de 200 testes lógicos separados. Você pode usar um conjunto, por exemplo. Quase qualquer linguagem de programação possui uma incorporada:
E se você receber uma entrada que não seja um rolo Yahtzee válido, jogue como se não houvesse amanhã.
fonte