Introdução

O Test Driven Development (TDD) às vezes é descrito como “escrevendo testes primeiro”. O mantra TDD afirma que não devemos escrever código antes de escrevermos testes automatizados que exercitam esse código. Escrever código primeiro é considerado subótimo.

E, é claro, escrever o código primeiro é como desenvolvemos o software seguindo o chamado modelo em cascata. Nesse modelo, dividimos as atividades de desenvolvimento de software em estágios. Por exemplo, temos um estágio de “levantamento de requisitos”, um estágio de “construção de aplicativos”, um estágio de “teste de aplicativos”, um estágio de “implantação de aplicativos” e assim por diante.

Mas como isso difere da metodologia ágil? Não temos exatamente os mesmos estágios no Agile? Claro que nós fazemos. A diferença crucial é que, no ágil, esses estágios não são bloqueados. Em cascata, fechamos as etapas e as executamos em sequência estrita. Ou seja, não começaremos a criar o aplicativo de remessa até que os requisitos sejam reunidos, concluídos, assinados e congelados. Depois que os requisitos são congelados (e controlados por nossas políticas de gerenciamento de mudanças), passamos para o próximo estágio (ou fase) – criação de aplicativos. Da mesma forma, não entraremos no estágio de teste até que todo o aplicativo tenha sido construído e atingimos o marco completo do código, quando as alterações no código foram congeladas. Depois que o código é congelado (e o congelamento de código é controlado por nossas políticas de gerenciamento de alterações), nós o entregamos aos testadores. A fase de teste começa e, somente após a conclusão de todos os testes (e não fornecendo defeitos significativos foram detectados), passamos para a fase de implantação.

No ágil, realizamos todas as atividades acima em paralelo. Ao mesmo tempo. Continuamos trabalhando nas histórias do usuário (especificações) enquanto construímos simultaneamente o aplicativo de remessa. Enquanto criamos o aplicativo, também o estamos testando. Além disso, como estamos construindo e testando o aplicativo, também o estamos implantando. Aprendemos com o aplicativo de remessa implantado na produção e usamos esse aprendizado validado como o feedback que informará as novas histórias de usuários. Dessa forma, o loop é fechado e estamos iterando, melhorando o valor de forma incremental.

A única maneira de habilitar a entrega iterativa desse fluxo de valor é confiando em testes automatizados. E, como descrevemos, esses testes estão sendo escritos muito cedo no jogo; na verdade, os testes devem ser escritos antes de escrevermos o código de remessa.

Por que, então, o título deste artigo é “Não escreva testes primeiro, escreva um teste primeiro”? Parece um pouco confuso. Vamos descompactar o significado deste título.

Um exemplo simples

Para entender a diferença entre escrever testes primeiro e escrever um teste primeiro, pode ser melhor mostrar do que apenas contar. Então, vamos tentar criar um exemplo simples: para este exercício, escolhi um caso trivial de calcular uma dica em um restaurante. Muitas vezes, nos encontramos em uma posição em que queremos dar gorjeta ao restaurante pelo serviço, mas é difícil calcular porcentagens em nossa cabeça. Então, um pouco bacana Tip Calculator pode ser útil.

Aqui estão as expectativas:

Como patrono
Quero calcular a conta total (total mais a gorjeta)
Porque eu quero elogiar o restaurante pelo serviço

Cenário # 1: Patrono calcula o total para serviços terríveis
Dado que o total do restaurante é de US $ 100,00
E o serviço foi terrível
Quando a calculadora da ponta calcula a carga total
Em seguida, a calculadora de gorjetas mostra $ 100,00 de carga total

Cenário # 2: o consumidor calcula o total para serviços inadequados
Dado que o total do restaurante é de US $ 100,00
E o serviço foi ruim
Quando a calculadora da ponta calcula a carga total
Em seguida, a calculadora de gorjetas mostra $ 105,00 de carga total

Cenário 3: O consumidor calcula o total para um bom serviço
Dado que o total do restaurante é de US $ 100,00
E o serviço foi bom
Quando a calculadora da ponta calcula a carga total
Em seguida, a calculadora de gorjetas mostra $ 110,00 de carga total

Cenário # 4: o Patrono calcula o total para um ótimo serviço
Dado que o total do restaurante é de US $ 100,00
E o serviço foi ótimo
Quando a calculadora da ponta calcula a carga total
Em seguida, a calculadora de gorjetas mostra $ 115,00 de carga total

Cenário # 5: o Patrono calcula o total para um serviço excelente
Dado que o total do restaurante é de US $ 100,00
E o serviço foi excelente
Quando a calculadora da ponta calcula a carga total
Em seguida, a calculadora de gorjetas mostra $ 120,00 de carga total

Vamos agora implementar a história do usuário acima (consulte o Barra Lateral para detalhes sobre a pilha de tecnologia).

Vemos que a história tem 5 critérios de aceitação (também conhecidos como cenários). Agora passamos para a fase de análise – pense sobre qual deve ser a primeira funcionalidade que nosso Tip Calculator aplicação deve implementar. Mas primeiro, vamos abrir o terminal da linha de comando e criar o novo diretório:

md TipCalculator

cd TipCalculator

e criar aplicativo e testes diretórios dentro do TipCalculator diretório.

Agora testes de cd e corra:

dotnet new xunit

Então cd .. e aplicativo de cd, então corra:

dotnet new classlib

Agora estamos prontos para dançar!

Abra seu editor de texto favorito (o meu é Código do Visual Studio) e concentre-se nas expectativas. Que comportamento estamos esperando do Tip Calculator?

Para limitar o escopo de nossas expectativas, geralmente ajuda a adotar um critério de aceitação (ou seja, um cenário) e focar nele primeiro. Vamos pegar o cenário 1:

Cenário # 1: Patrono calcula o total para serviços terríveis
Dado que o total do restaurante é de US $ 100,00
E o serviço foi terrível
Quando a calculadora da ponta calcula a carga total
Em seguida, a calculadora de gorjetas mostra $ 100,00 de carga total

Caso o serviço tenha sido terrível, não adicionaremos nenhuma dica e Tip Calculator está calculando uma gorjeta de US $ 0,00. Então, como automatizamos esse cenário?

Minha primeira expectativa seria que, de alguma forma, informassemos o Tip Calculator que o serviço foi terrível. Digitamos a palavra “Terrível” no campo de entrada ou selecionamos “Terrível” na lista de classificações de serviço disponíveis. Portanto, a primeira coisa a fazer aqui é articular algumas expectativas em relação a Tip CalculatorA capacidade de ser notificado de que o serviço foi terrível.

Eu gosto de começar sempre com a expectativa de que o que o usuário insere é válido. Então, primeiro escrevi um teste que verifica se a classificação “Terrível” é reconhecida pelo Tip Calculator como uma classificação válida. Vou ao testes diretório, renomeie o UnitTest1.cs arquivo para TipCalculatorTests.cs e adicione o seguinte teste:

[Fact]
public void CheckIfRatingTerribleIsValid () {
varpectedResponseForValidRating = true;
var actualResponseForValidRating = false;
Assert.Equal (pectedResponseForValidRating, actualResponseForValidRating);
}

Agora vá para a linha de comando, testes de cd, e corra:

teste dotnet

image 21

Obviamente, o teste trivial acima falhará, porque codificamos os valores. Mas é sempre uma boa prática garantir que nossos testes falhem antes de prosseguir. Não observar uma falha no teste pode nos dar uma falsa sensação de segurança mais tarde, se nenhum teste falhar e acabamos pensando que tudo funciona como esperado.

Mais algumas observações sobre o teste acima:

  • Ajuda se o nome do teste for descritivo. Eu escolhi CheckIfRatingTerribleIsValid para comunicar o fato de que devemos garantir que nosso aplicativo seja capaz de reconhecer nossos comandos.
  • Também ajuda se os nomes de variáveis ​​esperados e reais forem descritivos. Eu escolhi pectedResponseForValidRating e actualResponseForValidRating como bastante indicativo de qual é a nossa expectativa neste teste e também qual é o valor real Tip Calculator produzir.
  • O teste é um código-fonte de primeira classe e deve ser abordado com o mesmo cuidado aplicado ao código de produção.

Primeira decisão de projeto

Neste ponto, somos forçados a tomar uma decisão – como será a nossa nascente Tip Calculator sabe se a classificação de serviço fornecida pelo usuário é válida ou não? A decisão de design que vem à mente é que Tip Calculator deve poder armazenar e recuperar alguns dados. Nesse caso, os dados nos quais estamos interessados ​​são a classificação do serviço.

Se voltarmos à história do usuário e revisarmos os cinco critérios de aceitação, veremos que as expectativas são Tip Calculator deve ser capaz de reconhecer cinco classificações de serviço diferentes:

  1. Terrível
  2. Pobre
  3. Boa
  4. Ótimo
  5. Excelente

Portanto, a maneira mais simples de obter Tip Calculator armazenar essas informações seria dotá-las de uma matriz ou de uma lista. Mas, em vez de nos apressarmos em implementar essa lista, devemos examinar as expectativas novamente, para ver se há mais alguma coisa que possamos ter perdido. E existe – não apenas deve Tip Calculator para poder reconhecer classificações de serviço válidas, também deve ser capaz de associar cada classificação a um valor percentual. Nossa análise mostra as seguintes associações:

  1. Terrível => 0%
  2. Fraco => 5%
  3. Bom => 10%
  4. Ótimo => 15%
  5. Excelente => 20%

Nesse caso, uma matriz simples ou uma lista simples não serão suficientes para manter as associações acima. Qual é a próxima estrutura de dados mais simples que nos permitirá implementar essas associações? Depois de fazer um pouco de pesquisa, descobrimos que Hashtable é provavelmente a estrutura de dados mais adequada que pode atender às nossas necessidades com a menor quantidade de cerimônia.

Agora navegamos para o aplicativo diretório e renomear Class1.cs arquivo para TipCalculator.cs. Agora queremos adicionar um Hashtable que manterão classificações de serviço e os valores percentuais associados:

System.Collections.Hashtable ratingPercentages = novo System.Collections.Hashtable ();

Agora é um bom momento para lembrar que o TDD está focado em acoplar as expectativas ao comportamento do aplicativo, não à estrutura do aplicativo. Sabendo disso, precisamos modificar nosso teste para tornar Tip Calculator exibem algum comportamento. O teste codifica algumas expectativas em relação à forma como o aplicativo deve se comportar, e o aplicativo em execução fornece a evidência do comportamento esperado.

Mas qual é a evidência do comportamento do aplicativo? Não há outra maneira de avaliar e avaliar o comportamento do aplicativo, a não ser examinando os valores que o aplicativo em execução produz. Nesse caso, esperamos que o aplicativo em execução produza valores verdade ou falso (Valores booleanos) após solicitarmos ao aplicativo se determinado valor (ou seja, classificação de serviço) é válido.

Para ensinar o aplicativo a se comportar da maneira esperada, precisamos dotar uma API. Nesse caso, projetamos a API da seguinte maneira:

public bool CheckIfRatingIsValid (classificação de sequência de caracteres)

Em nosso teste, modificaremos o valor esperado real para exercitar o aplicativo em execução e coletar o valor de saída:

Como você pode ver na captura de tela acima, instanciamos o TipCalculator, mas ao tentar solicitar à instância que verifique se a classificação fornecida (“Terrível”) é válida, o editor está reclamando que não consegue encontrar esse método.

Bem, é claro, o método ainda não foi implementado. Agora é a hora de seguir em frente e fazê-lo:

public bool CheckIfRatingIsValid (classificação de sequência de caracteres) {
retorna falso;
}

Agora que o método está implementado, o teste funciona; aqui está a lista completa:

usando Xunit;
usando app;

testes de namespace {
classe pública TipCalculatorTests {
TipCalculator tipCalculator = new TipCalculator ();

[Fact]
public void CheckIfRatingTerribleIsValid () {
varpectedResponseForValidRating = true;
var actualResponseForValidRating = tipCalculator.CheckIfRatingIsValid (“Terrible”);
Assert.Equal (pectedResponseForValidRating, actualResponseForValidRating);
}
}
}

Vemos no exemplo acima que estamos trapaceando novamente (codificamos retorno falso; em nosso método recém-cunhado). Qual é o sentido de contornar e simplesmente criar esqueletos e andaimes em vez de arregaçar as mangas e fazer codificação real? Vamos discutir esse importante tópico.

Discussão sobre nossa primeira decisão de design

Estamos ilustrando aqui como fazer o TDD passo a passo. O engraçado é que essa ilustração passo a passo é realmente a maneira exata como fazemos TDD: passo a passo. Não há outra maneira de fazer TDD do que fazê-lo passo a passo. Um passo de cada vez.

Qual é a diferença de qualquer outra maneira de desenvolver software? Também não fazemos tudo passo a passo, mesmo quando não seguimos a metodologia TDD? Bem, na verdade não. Deixe-me explicar:

TDD para mim é como andar a cavalo a galope. Estamos nos movendo rapidamente em direção ao nosso objetivo, mas frequentemente tocamos o chão (o cavalo galopando está de vez em quando batendo no chão para saltar e correr rápido). Em comparação, quando estou desenvolvendo software sem TDD, parece que estou empinando uma pipa. Estou fazendo movimentos rápidos com a pipa, mas nunca toco o chão, nem mesmo uma vez. No momento em que empresto a pipa, o local de pouso pode não estar onde eu pretendia ir (é muito difícil controlar a direção de uma pipa se estiver voando com um vento forte).

Com o TDD, sempre que fazemos uma alteração no código (tanto o código de teste quanto o código do aplicativo de remessa), executamos os testes e tocamos o solo. Estamos galopando, mas ao mesmo tempo precisamos desse aterramento frequente. Precisamos ver se estamos indo na direção certa e também se quebramos alguma coisa durante nosso galope. Nossos testes são o Oracle que continua nos dizendo se tudo funciona como esperado ou se algo começou a se comportar mal.

Fazer alterações no código é um negócio arriscado. O TDD fornece um arnês agradável que orienta nossas decisões de projeto e garante que não atrapalhemos algo que já confirmamos que funciona de acordo com nossas expectativas.

Substitua o valor codificado pela lógica de processamento real

Vamos agora substituir o valor codificado pelo código em execução real. Ensinamos primeiro a nossa Tip Calculator que existe uma classificação de serviço chamada “Terrível” e que a porcentagem de gorjeta associada a essa classificação é 0:

public bool CheckIfRatingIsValid (classificação de sequência de caracteres) {
ratingPercentages.Add (“Terrível”, 0);
retorna falso;
}

Nosso Tip Calculator agora conhece o fato de que há uma classificação de serviço rotulada como “Terrível” e a porcentagem de gorjeta associada a um serviço terrível é 0%. Ótimo, mas ainda estamos retornando valor codificado (falso) Hora de substituí-lo pelo cálculo real:

public bool CheckIfRatingIsValid (classificação de sequência de caracteres) {
ratingPercentages.Add (“Terrível”, 0);
return ratingPercentages.ContainsKey (rating);
}

Execute o teste novamente:

image 22

Ótimo, mas o código ainda parece artificial. Estamos carregando o valor “Terrível” na instância de Hashtable (ratingPercentages) e verifique imediatamente se esse valor existe no campo Hashtable. Agora que passamos do teste que falhou (Vermelho) para o teste que passou (Verde), é hora de executar a terceira etapa do loop TDD – Refatorar.

A refatoração é basicamente a atividade de modificar a estrutura do código sem afetar o comportamento do código. Nossa tarefa aqui é simples: extrair o código responsável pelo preenchimento do Classificação Hashtable em um bloco de código separado. O local mais natural para esse carregamento está no bloco de código que está iniciando a inicialização do Tip Calculator – a construtor método. Após a refatoração, nosso código-fonte do aplicativo de remessa fica assim:

using System.Collections;

aplicativo de namespace {
classe pública TipCalculator {
private Hashtable ratingPercentages = new Hashtable ();
public TipCalculator () {
ratingPercentages.Add (“Terrível”, 0);
}

public bool CheckIfRatingIsValid (classificação de sequência de caracteres) {
return ratingPercentages.ContainsKey (rating);
}
}
}

Execute o teste novamente e ele passa (estamos em verde). Modificamos a estrutura do código sem modificar seu comportamento! Bom trabalho.

Jogue a moeda

Sempre que satisfazemos uma expectativa positiva, é uma prática prudente inverter as coisas e descrever a expectativa negativa. Neste ponto, já que consideramos que um valor legítimo de classificação de serviço é encontrado no Tip Calculator, queremos garantir que valores não legítimos não sejam encontrados no Tip Calculator. O que queremos dizer com valores não legítimos? Qualquer valor que não seja “Terrível”, “Ruim”, “Bom”, “Ótimo” e “Excelente”. Hora de escrever a nova expectativa (ou seja, teste):

[Fact]
public void CheckIfRatingWhateverIsValid () {
varpectedResponseForValidRating = true;
var actualResponseForValidRating = tipCalculator.CheckIfRatingIsValid (“Whatever”);
Assert.Equal (pectedResponseForValidRating, actualResponseForValidRating);
}

Execute os testes:

image 23

Falha. Conforme o esperado (especificamos que nossa expectativa ao fornecer a classificação de serviço como “Qualquer que seja” deve ser verdade; na realidade, é falsoporque nosso Tip Calculator não contém o valor “Whatever”).

Corrija o teste (altere o pectedResponseForValidRating de verdade para ˆ) e execute-o novamente:

image 24

Um momento de reflexão: por que falsificamos o primeiro teste e o fizemos falhar? Porque sempre queremos ter certeza de que nosso novo teste falhará. Dessa forma, saberemos que no futuro qualquer aprovação bem-sucedida no teste não será apenas um falso positivo.

Em louvor ao estado estacionário

Engenharia de software é um ato de equilíbrio entre estado estacionário e períodos de estado instável. O que queremos dizer com estado estacionário? Se tivermos um sistema (um aplicativo em execução) que se comporta da maneira que esperamos que ele se comporte (ou seja, produz valores que especificamos como valores esperados), declaramos que o sistema está em um estado estável. Está sendo executado e está fornecendo algum valor. Essa entrega de valor ainda é parcial; no nosso caso, o único valor para os usuários que Tip Calculator entrega é sua capacidade de reconhecer a classificação de serviço “Terrível” como uma classificação legítima; além disso, é capaz de nos informar que a classificação do serviço “Qualquer que seja” não é uma classificação legítima.

Isso não é muito, mas ainda é melhor que nada. E boas notícias – nosso aplicativo em execução está atualmente em um estado estável. Agora queremos ver como adicionar um comportamento mais valioso ao nosso Tip Calculator. E a única maneira de agregar mais valor é fazendo algumas alterações.

Sempre que fazemos uma alteração em nosso aplicativo, perturbamos seu estado estacionário. Esse distúrbio é arriscado. Isso pode significar que nossas alterações podem quebrar algo que já está funcionando. Por causa dessa preocupação, esforçamo-nos para tornar a duração desse estado instável o mais curta possível. Lembra como comparamos o TDD com o cavalo a galope? Quando o cavalo está voando (ou seja, não está tocando o chão), está avançando em direção ao nosso objetivo, mas não está em estado estacionário. Somente quando o cavalo toca o chão é que seu estado se estabiliza.

O TDD incentiva a fazer pequenas alterações (em voo) e aterrar imediatamente o sistema, verificando se ele está de volta ao estado estacionário. Valorizamos o estado estacionário, apesar do fato de abraçarmos ansiosamente as mudanças. Sem alterações, não conseguiremos agregar valor, mas devemos fazê-lo de maneira muito deliberada e cuidadosa. Ao fazer TDD, tratamos as alterações para o estado estacionário como caminhar sobre cascas de ovos. Não importa o quão certo tenhamos em saber o que e como estamos fazendo a engenharia de software, é prudente ainda permitir que os testes com falha guiem nossas decisões.

Verifique se a porcentagem correta da ponta está associada à classificação do serviço

Vamos agora introduzir outra alteração em nosso aplicativo – um teste para verificar se a porcentagem correta de gorjeta está associada à classificação de serviço “Terrível”. Lembre-se de que preenchemos a instância de Classificação Hashtable com os seguintes valores:

ratingPercentages.Add (“Terrível”, 0);

Escrevemos um teste que verifica se nossos Classificação Hashtable contém classificação de serviço legítimo “Terrível”. Agora, precisamos de um teste que verifique se a classificação do serviço “Terrível” significa que a porcentagem da dica para essa classificação é 0.

[Fact]
public void CheckIfRatingTerribleHasZeroPercentTip () {
var esperadoZeroPercentForTerribleRating = 0;
var actualZeroPercentForTerribleRating = 10;
Assert.Equal (allowedZeroPercentForTerribleRating, atualZeroPercentForTerribleRating);
}

O novo teste (CheckIfRatingTerribleHasZeroPercentTip) deve falhar:

image 25

Novamente, codificamos propositadamente valores reais errados, apenas para que pudéssemos observar nosso novo teste falhar. Agora, devemos substituir o valor codificado pela chamada real para o Tip CalculatorO método que retorna a porcentagem de gorjeta para a classificação do serviço:

[Fact]
public void CheckIfRatingTerribleHasZeroPercentTip () {
var esperadoZeroPercentForTerribleRating = 0;
var actualZeroPercentForTerribleRating = tipCalculator.GetPercentageTipForRating (“Terrible”);
Assert.Equal (allowedZeroPercentForTerribleRating, atualZeroPercentForTerribleRating);
}

Como no caso anterior, inventamos uma nova API para Tip Calculator; chamamos isso de nova capacidade GetPercentageTipForRating (“Terrible”). Ele pega o valor da classificação de serviço e retorna a porcentagem de gorjeta para essa classificação.

Vire para o app / TipCalculator.cs e adicione o esqueleto codificado do novo método:

public int GetPercentageTipForRating (classificação de sequência de caracteres) {
retornar 10;
}

A execução do teste falha novamente, porque codificamos o valor de retorno; vamos substituí-lo pelo processamento real:

public int GetPercentageTipForRating (classificação de sequência de caracteres) {
int tipPercentage = Int32.Parse (ratingPercentages[rating].Para sequenciar());
tipPercentage de retorno;
}

Execute o teste novamente:

image 26

Todos os três testes passam; estamos de verde, voltamos ao estado estacionário!

Que porcentagem de gorjeta esperamos para classificações de serviço não legítimas?

Muitos anos de experiência no campo me ensinaram a ser um pouco pessimista. Agora que o aplicativo está de volta ao estado estacionário, fornecendo valor (respondendo a perguntas sobre classificações de serviço legítimas e também fornecendo a porcentagem correta de gorjeta para a classificação “Terrível”), precisamos ver o que acontece quando executamos o aplicativo, fornecendo-o valor de classificação de serviço não legítimo (por exemplo, atribuindo a classificação de serviço “Tanto faz”).

Hora de deixar o estado estacionário mais uma vez; vamos escrever outro teste:

[Fact]
public void CheckIfRatingWhateverHasNegativeOnePercentTip () {
varpectedZeroPercentForWhateverRating = -1;
var actualZeroPercentForWhateverRating = tipCalculator.GetPercentageTipForRating (“Whatever”);
Assert.Equal (pectedZeroPercentForWhateverRating, realZeroPercentForWhateverRating);
}

Estamos descrevendo nossa expectativa quando Tip Calculator é solicitado que você retorne a porcentagem da dica para a classificação de serviço “Tanto faz”. Como a classificação de serviço “Qualquer que seja” é uma classificação não legítima, estamos esperando Tip Calculator para retornar a porcentagem da dica do valor -1.

Este teste agora precipita uma melhoria no nosso código de remessa. Precisamos adicionar alguma lógica para verificar primeiro se a classificação do serviço fornecido é legítima ou não. Somente se for legítimo, pedimos Classificação Hashtable para nos dizer qual é o valor associado da porcentagem da dica. Se a classificação do serviço fornecido não for legítima (por exemplo, se for “Tanto faz”), ignoramos a conversa com Classificação Hashtable e simplesmente retorne -1 negativo.

public int GetPercentageTipForRating (classificação de sequência de caracteres) {
int tipPercentage = -1;
if (CheckIfRatingIsValid (rating)) {
tipPercentage = Int32.Parse (ratingPercentages[rating].Para sequenciar());
}
tipPercentage de retorno;
}

Execute os testes e os quatro testes passam:

image 27

Estamos de volta ao estado estacionário. Outra curta excursão à área volátil, outra vitória rápida e um retorno seguro ao estado estável e imperturbável.

Preencher outras porcentagens de gorjeta de classificação de serviço

Agora é uma boa hora para respirar e fazer mudanças menos arriscadas, seguindo o padrão já estabelecido. Deixe a segurança do estado estacionário e faça pequenas viagens ao território volátil, adicionando um novo teste para verificar se a classificação de serviço “Fraco” é uma classificação válida e legítima:

[Fact]
public void CheckIfRatingPoorIsValid () {
varpectedResponseForValidRating = true;
var actualResponseForValidRating = tipCalculator.CheckIfRatingIsValid (“Fraco”);
Assert.Equal (pectedResponseForValidRating, actualResponseForValidRating);
}

A execução deste teste falhará:

image 28

A classificação de serviço “Insatisfatória” ainda não foi implementada. Para fazer o teste passar, implemente a classificação de serviço “Fraca” adicionando esta linha ao TipCalculator construtor:

ratingPercentages.Add (“Fraco”, 5);

Execute os testes e voltamos à segurança:

image 29

Estamos desfrutando do estado estacionário, com seis testes aprovados com sucesso.

Agora que adicionamos a classificação de serviço “Insatisfatória” associada à gorjeta de 5%, vamos escrever um teste que descreva essa expectativa:

[Fact]
public void CheckIfRatingPoorHasFivePercentTip () {
var expectZeroPercentForPoorRating = 5;
var actualZeroPercentForPoorRating = tipCalculator.GetPercentageTipForRating (“Fraco”);
Assert.Equal (allowedZeroPercentForPoorRating, actualZeroPercentForPoorRating);
}

Os testes são executados com sucesso e voltamos a estar seguros no estado estacionário.
Deixo ao leitor as alterações que conduzirão à implementação das classificações de serviço “Bom”, “Ótimo” e “Excelente”. No final do exercício, você deve ter seu sistema de volta ao estado estacionário, com 12 testes aprovados com êxito:

image 35

Calcular o total geral, considerando o total e a classificação do serviço

Agora estamos prontos para a etapa final – considerando a conta total e a classificação do serviço, esperamos Tip Calculator para calcular a porcentagem da gorjeta e adicioná-la ao total, produzindo o total geral a ser pago ao restaurante.

Como sempre fazemos, primeiro descrevemos a expectativa:

[Fact]
public void CalculateTotalWithTip () {
varpectedTotalWithTip = 135,7;
var actualTotalWithTip = 200.0;
Assert.Equal (pectedTotalWithTip, actualTotalWithTip);
}

Como sempre, primeiro codificamos algumas expectativas que sabemos que vão falhar; isto é para que observemos falha no nosso novo teste:

image 36

Hora de implementar a lógica de processamento que calculará o total correto com a ponta. Dado o total de US $ 118,0 e a classificação de serviço “Ótimo” (dica de 15%), esperamos que o total seja 135,7:

[Fact]
public void CalculateTotalWithTip () {
var rating = “Ótimo”;
var total = 118;
varpectedTotalWithTip = 135,7;
var actualTotalWithTip = tipCalculator.CalculateTotalWithTip (total, classificação);
Assert.Equal (pectedTotalWithTip, actualTotalWithTip);
}

Nós projetamos uma nova API Tip Calculator – um método chamado CalculateTotalWithTip (total, classificação). Ele pega o valor total e a classificação do serviço e retorna o total com gorjeta. A implementação do método é assim:

public double CalculateTotalWithTip (total duplo, classificação de string) {
totalWithTip duplo = -1;
if (CheckIfRatingIsValid (rating)) {
int percent = GetPercentageTipForRating (classificação);
totalWithTip = total + ((total / 100) * porcentagem);
}
return totalWithTip;
}

Execute os testes e voltamos ao estado estacionário:

image 37

Nós terminamos aqui?

Não, ainda não. Mesmo quando todos os testes estão em verde e estamos de volta ao estado estacionário, ainda há algumas coisas que precisamos fazer. Para começar, precisamos adicionar uma expectativa pessimista para nossos Tip Calculator cálculo do total com a gorjeta com base na classificação do serviço:

[Fact]
public void CalculateTotalWithTipForNonlegitimateRating () {
var rating = “Meh”;
var total = 118;
varpectedTotalWithTip = 135,7;
var actualTotalWithTip = tipCalculator.CalculateTotalWithTip (total, classificação);
Assert.Equal (pectedTotalWithTip, actualTotalWithTip);
}

A execução dos testes produz um teste com falha:

image 38

Nossa expectativa de classificação de serviço não legítima (“Meh”) estava incorreta. O total real é -1, por isso precisamos ajustar nossa expectativa substituindo 135,7 por -1. Execute os testes novamente e voltamos ao estado estacionário!

image 39

Agora temos 14 testes, todos eles são aprovados com êxito e nosso Tip Calculator trabalha de acordo com nossas expectativas e atende aos critérios de aceitação.

Estamos quase terminando. Mais uma verificação de sanidade antes que possamos enviar com confiança nosso novo e brilhante Tip Calculator – devemos correr teste de mutação. Nossa estrutura de teste de mutação modifica o código de remessa, uma linha de cada vez, e executa todos os testes para cada mutação individual. Se os testes se queixarem do código mutado, tudo está bem, matamos o mutante. Se os testes não se queixam, estamos com problemas. Temos um mutante sobrevivente em nossa base de código, o que significa que há linhas de código em nosso repositório que estão fazendo algo para o qual não fornecemos nenhuma expectativa.

Vamos executar o teste de mutação para ver quão sólida é a nossa solução. Boas notícias – nossa solução matou 100% dos mutantes!

image 40

O teste de mutação deu ao nosso aplicativo de remessa um atestado de integridade. Nosso Tip Calculator parece estar em boa forma.

Refator-Vermelho-Verde-Refletir

Vamos revisar nossa Tip Calculator exercício de construção. Iniciamos o processo descrevendo nossas expectativas usando o formato clássico de história do usuário. A história do usuário (como o nome indica) é focada na descrição de cenários que atendem às metas do usuário final. Nesse caso, o objetivo simples é calcular o valor da gorjeta a partir da classificação do serviço fornecido e do total da fatura do restaurante. O valor da gorjeta calculado é automaticamente adicionado ao total.

A partir daí, começamos a construir nosso aplicativo de remessa seguindo a metodologia TDD. Como demonstramos, a metodologia consiste em escrever um teste que falhou, observá-lo falhar (a fase vermelha do TDD) e, em seguida, fazer alterações de código imediatamente que garantem a aprovação do teste (a fase verde do TDD). Depois que o teste passa, passamos para a fase Refatorar (reestruturamos o código sem afetar seu comportamento). Dessa forma, garantimos que nosso código não seja caro para alterar.

Uma prática adequada de TDD também exige freqüentes retrospectivas – chamamos isso de reflexão. Paramos e pensamos nas coisas que realizamos até agora, para ver se poderíamos aprender com nossas experiências recentes. Essa reflexão fortalece o processo, pois se baseia em feedback frequente e rígido fornecido pelos testes que falharam e, em seguida, pelos sucessivos.

Eu já comparei o Test Driven Development com a experiência de andar a cavalo a galope. Ao andar a cavalo, alternamos entre voar pelo ar (ou seja, a velocidade alcançada quando o cavalo pula do chão) e guiá-lo. É impossível dirigir o cavalo enquanto estamos fora do chão, no ar. Nesse ponto, ganhamos velocidade, mas não podemos fazer nenhuma alteração na direção. Somente quando o cavalo toca o chão é que podemos mudar de direção.

No TDD, nos esforçamos para tocar o chão com a maior frequência possível. Quanto maiores os saltos que fazemos sem tocar o chão, menor a chance de corrigir o percurso.

Também comparei práticas de desenvolvimento de software que não seguem os princípios do TDD com a experiência de empinar pipa. Ao empinar uma pipa, nunca tocamos o chão. É uma sensação emocionante de deixar o vento pegar a pipa e jogá-la no ar. Podemos alcançar uma velocidade considerável dessa maneira. Mas lutamos nessas situações para manter o rumo desejado. E depois que finalmente pousamos a pipa, ela geralmente não pousa no local que originalmente queríamos que pousasse.

Por que a ênfase deste artigo é em “não escreva testes primeiro”? Muitos engenheiros de software que não estão familiarizados com as práticas ágeis implementadas no TDD geralmente afirmam que não é necessário escrever testes automatizados ou afirmam que testes automatizados devem ser gravados após a conclusão do código. Quando começarem a aprender sobre o Agile e o TDD, eles poderão reconsiderar suas práticas e decidir que escrever testes antes de escrever o código de implementação pode fazer mais sentido. Ainda assim, devido à mentalidade arraigada da cachoeira, alguns desses engenheiros cometem o erro de escrever todos os testes primeiro e só depois passam a escrever o código.

Essa abordagem está completamente errada. It is equivalent to the traditional waterfall approach where we go through the development process by respecting gated phases. First we write the requirements (in this case, requirements would be expectations written in the form of automated tests). Only once all the requirements (i.e. automated tests) have been written, signed off and frozen, do we move into the next gate phase – write the code for the shipping application.

TDD is the exact opposite of the “write tests first” approach. In TDD, we always write only one test. That test describes a desired behaviour. The desired behaviour does not exist yet (that’s why it is desired), and the test fails. We then immediately move into making changes to the code in the attempt to create the desired behaviour. Once desired behaviour is created, it gets validated by the test, and if the expectations of the test are satisfied, we move into refactoring the code (to satisfy nonfunctional requirements, such as cost of change).

We practice a rigorous discipline to never succumb to the temptation to write more than one test at a time. That way, we ensure that we keep touching the ground as frequently as possible. We prefer to remain ‘in flight’ for the shortest possible time. We are ‘in flight’ during that period when the desired behaviour described in the test has not materialized yet. The smaller the expected and desired behaviour is, the shorter will be our ‘in flight’ trajectory. That way, we keep touching the ground often, which gives us a chance to adjust the steering.

Conclusão

Building a simple Tip Calculator is a toy sized problem, and using that exercise to illustrate TDD methodology is not necessarily providing a convincing argument in favour of TDD. Still, within the constraints of a technical article, going over this hands-on exercise may provide valuable insights into the benefits of adopting TDD.

We would still argue that the real benefits of TDD only become apparent when dealing with much larger, more complex software engineering efforts. The ability to remain grounded while making potentially risky changes to a large, complex system is often a life saver. In addition to that, building software using TDD methodology results in much less rework. TDD drives high degree of modularization, which results in high cohesiveness of the modules and low coupling between the modules. All these characteristics produce a shipping application whose codebase is easy and inexpensive to change. And lowering the cost of change has proven to be the best way on the path to embracing changes and abandoning the concept known as ‘scope creep’. Bottom line, TDD enables software engineering teams to deliver high degree of flexibility to the business.

Technology stack used for this exercise: in the attempt to keep the exercise simple and easy to follow, I have chosen .NET Core platform, together with xUnit.net testing platform. To follow the coding examples, please install .NET Core e xUnit.net.

In order to be able to run the sample code, please open ./tests/tests.csproj file and add this line to the ItemGroup:

You’re now all set for following the coding exercises.



Fonte