Pontos Principais
- Qualquer software deve ser testado para garantir que atenda aos requisitos funcionais, mas também devemos testar os requisitos não-funcionais como segurança, usabilidade e - com bastante ênfase em - manutenibilidade;
- Desenvolvimento guiado por testes (TDD, do inglês Test-driven development) é uma técnica estabelecida para entregar um software melhor, com mais rapidez e mais sustentavelmente ao longo do tempo;
- TDD é baseado em uma ideia simples: Escreva um teste que falhe antes de criar o código de produção. Entretanto, essa ideia "simples" requer habilidade e entendimento para ser bem executada;
- TDD é de fato uma técnica de projeto. Suas fundações focam em usar pequenos testes para projetar sistemas do zero de forma emergente, e rapidamente obter valor enquanto gera confiança no sistema. Projeto guiado por testes (Test-Driven Design) seria um nome melhor para esta técnica;
- Geralmente o primeiro passo para desenvolver uma solução para um determinado problema, seja de qualquer nível de complexidade, é analisá-lo e quebrá-lo em componentes menores. Estes componentes podem ser implementados em uma sequência de passos, considerando ao mesmo tempo tanto os cenários de entrada como suas saídas.
Precisamos testar o software para garantir que atenda aos requisitos, que responda corretamente a entrada (validação de entrada), que tenha um tempo de processamento aceitável (teste de desempenho), que os usuários consigam instalá-lo e utilizá-lo (teste de implantação) e de acordo com os objetivos dos stakeholders. Estes objetivos podem ser resultados de negócios ou funções como segurança, usabilidade, manutenibilidade e outros tipos de características.
Os tipos de teste incluem:
- Testes que verificam se o software funciona mesmo em uma forma básica;
- Teste contínuo é executado a cada iteração, como quando executamos o Maven;
- Usamos teste de regressão quando adicionados um código novo ou alteramos o código existente. Queremos garantir que o restante do código continue funcionando;
- Testes de desempenho medem o tempo de execução do software;
- Testes de aceitação verificam se os stakeholders estão satisfeitos com o software e dispostos a pagar por ele.
Testes unitários são os menores blocos usados na construção do conjunto de testes. Todas as classes de um código têm uma classe de teste associada. O teste é isolado das outras classes simulando as chamadas dos métodos.
Os testes de integração são mais fáceis de implementar. Testamos uma classe com todas as dependências e posteriormente, podemos nos certificar de que o fluxo de execução está correto, mas quando o teste falha, não sabemos qual classe está falhando. Um teste de sistema verifica o sistema completo, incluindo hardware, sistema operacional, web services, entre outros.
Os testes deveriam ser legíveis para que não-programadores sejam capazes lê-los e modificá-los. Em uma equipe ágil, programadores trabalham junto aos testadores e analistas, e os testes e especificações são propriedade coletiva, de forma que todos deveriam ser capazes de ler os testes e até mesmo alterá-los quando necessário.
TDD: Tudo sobre design e produtividade
O Desenvolvimento Guiado por Testes (TDD) é uma técnica estabelecida para entregar softwares de maneira rápida, sustentável e de qualidade. O TDD se baseia em uma ideia simples: escreva um caso de teste antes de desenvolver o código de produção. Precisa de um novo comportamento? Escreva um teste. No entanto, essa ideia tão simples requer habilidade e entendimento para ser bem executada.
O TDD é na verdade uma técnica de projeto, fundamentado no uso de pequenos testes para projetar o sistema de forma emergente e obter algum valor rápido enquanto criamos um software de confiança. Projeto guiado por testes seria um nome melhor.
Como um método de projeto, tudo se trata de foco e simplicidade. O objetivo é prevenir que os programadores escrevam códigos desnecessários para a entrega de valor. Se trata apenas de escrever a menor quantidade de código para resolver o problema.
Vários artigos exibem as vantagens do TDD e muitas apresentações em conferências nos falam para fazer os testes como sendo algo muito legal e divertido de ser feito. As vantagens que são comumente listadas do TDD são:
- Escrevemos um software melhor;
- Evitamos engenharia excessiva;
- Nos protegemos de quebrar todo o sistema quando introduzimos novas funcionalidades;
- Nosso software é auto-documentado.
Mesmo que sempre tivesse acreditado nessas vantagens, houve um tempo em que pensava que não precisava do TDD para escrever um software bom e de fácil manutenção. Agora, obviamente, sei que estava errado, mas porque pensava assim apesar dos benefícios mágicos? Devido ao custo!
O TDD é oneroso! Quem pensa que o custo é ainda maior quando não fazemos os testes está certo, mas esse custo chega em um momento diferente. Se fazemos TDD, temos um custo imediato, quando não o fazermos, o custo vem no futuro.
A forma mais efetiva de terminar alguma coisa é fazendo da forma mais natural possível. O natural das pessoas é serem preguiçosas, programadores são os melhores nisso, e gananciosas, então temos que encontrar uma forma de reduzir os custos imediatos. É fácil falar, mas muito difícil fazer!
Existem muitas teorias, dimensões e pontos de vista sobre o TDD, mas prefiro mostrar como utilizamos o TDD na prática. Por fim, veremos de onde partimos e para onde estamos indo até a obra de arte final, que só será alcançada com uso do TDD.
Aqui está o link da apresentação "Unit testing / TDD concepts with best practice guidelines", que contém os tópicos:
- Porque precisamos de testes;
- Tipos de testes;
- Como e quando usar cada tipo de teste;
- Cobertura por nível de teste;
- Estratégias de teste;
- TDD na prática.
A apresentação também inclui orientações e melhores práticas para ajudar o que fazer e não fazer nos testes. A seção "TDD in practice" e os conceitos apresentados no geral podem ser aplicados a qualquer linguagem, mas uso Java para demonstrar. O objetivo é mostrar como devemos pensar quando projetamos e criamos uma arte empolgante, e não apenas sair codificando.
Analise o problema
O primeiro passo para resolver qualquer problema, independente da complexidade, é analisá-lo e quebrá-lo em passos pequenos, contínuos e completos, considerando cenários de entrada e qual a saída esperada. Revisamos estes passos para ter certeza de que não há lacunas considerando os requisitos iniciais - nada a mais, nada a menos - de um ponto de vista de negócios, sem aprofundar em detalhes de implementação.
Este é um passo crítico. Um dos passos mais importantes é ser capaz de identificar todos os requisitos de um dado problema e agilizar a fase de implementação que está por vir. Tendo estes passos menores, podemos ter um código limpo e testável, facilmente implementável.
O TDD é a chave para desenvolver e manter estas etapas até cobrirmos todos os casos do problema em questão.
Imaginemos que foi pedido para desenvolver uma biblioteca que converte qualquer número romano em seu equivalente arábico. Como programador, farei o seguinte:
- Criar o projeto da biblioteca;
- Criar a classe;
- Provavelmente me aprofundar para criação de método de conversão;
- Pensar nos possíveis cenários do problema e o que pode ser feito;
- Escrever um caso de teste para a tarefag, assim arantindo tondo escrito cada dos testes possíveis,testes, mesmo que já tenha quase testado a tarefa no método principal, como de costume.
Para iniciar o processo corretamente e colocar o TDD em ação enquanto desenvolvemos o código, devemos seguir estes passos práticos para conseguirmos um projeto final bem sucedido, com um conjunto de casos de teste que poupa o tempo e custo de desenvolvimentos futuros.
O código para este exemplo pode ser clonado deste repositório no GitHub. Abra o terminal, vá até o diretório de sua preferência e execute o comando:
$ git clone https://github.com/mohamed-taman/TDD.git
Tomei cuidado para que o projeto tenha um commit para cada passagem de vermelho para verde do TDD, de forma que ao navegar pelos commits seja possível perceber as mudanças e a refatoração feitas para atender os requisitos do projeto final.
Estou usando Maven, Java SE 12 e JUnit 5.
TDD na prática
Para desenvolver o conversor, o primeiro passo é ter um caso de teste que converta o algarismo romano I no arábico 1.
Criamos a classe do conversor e a implementação do método para que o caso de teste satisfaça nosso primeiro requisito.
Como um conselho prático, é melhor começar com essa regra em mente: não crie primeiro o código, mas comece criando a classe e o método de teste. Isso se chama programação por intenção, onde nomeamos a nova classe e o novo método que serão usados para nos forçar a pensar sobre o uso do código que estamos escrevendo, o que definitivamente leva a projetos de APIs melhores e mais limpas.
Passo 1
Comece criando o pacote, a classe, o método e a implementação do teste:
Pacote:
rs.com.tm.siriusxi.tdd.roman
Classe:
RomanConvertTest
Método:
converIt()
Implementação:
assertEquals(1, new RomanConverter().convertRomanToArabicNumber("I"));
Passo 2
Não há um teste que está falhando, apenas um erro de compilação. Então vamos criar o pacote, a classe, o método no diretório de fontes do Java usando as sugestões da IDE.
Pacote:
rs.com.tm.siriusxi.tdd.roman
Classe:
RomanConverter
Método:
public int convertRomanToArabicNumber(String roman)
Passo 3 (Estado vermelho)
Precisamos nos certificar de que a classe e o método designados estejam corretos e que os testes executem perfeitamente.
Implementando o método convertRomanToArabicNumber
para lançar um IllegalArgumentException
, temos certeza de alcançar o estado vermelho.
public int convertRomanToArabicNumber(String roman) {
throw new IllegalArgumentException();
}
Execute o caso de teste. Devemos ver a barra vermelha.
Passo 4 (Estado verde)
Nesse passo, precisamos executar o teste novamente, porém dessa vez ver uma barra verde. Implementaremos o método com o mínimo de código para fazer o caso de teste ficar verde. Então o método deve retornar 1.
public int convertRomanToArabicNumber(String roman) {
return 1;
}
Execute o caso de teste. Devemos ver a barra verde.
Passo 5 (Refatoração)
Agora é hora da refatoração, se necessária. Gostaria de enfatizar que o processo de refatoração não envolve apenas o código de produção mas também o de teste.
Remova os imports não usados da classe de teste.
Devemos ver a barra verde novamente se tudo ainda estiver funcionando como esperado após a refatoração.
Remover o código não utilizado é a forma mais básica e simples de refatoração, o que melhora legibilidade do código e o tamanho da classe, e consequentemente o tamanho do projeto também.
Passo 6
A partir deste ponto, vamos seguir consistentemente o processo do vermelho para o verde. Passamos o primeiro ponto do TDD, o estado vermelho, escrevendo um novo requisito ou um passo do problema na forma de um caso de teste falho, e seguimos até completar a funcionalidade.
Note que começamos com um requisito funcional, ou passo, e então seguimos adiante, passo a passo, até terminarmos a funcionalidade necessária. Assim, temos passos claros para completar, começando do mais fácil e seguindo até o mais complexo.
É melhor fazer isso do que saltar adiante e gastar muito tempo pensando em toda a implementação de uma só vez, o que pode levar a pensar demais no assunto e cobrir casos que nem sejam necessários. Isso gera excesso de código. E faz com que sejamos menos produtivos.
O próximo passo é converter II em 2.
Na mesma classe de teste, criamos um novo método e a implementação como a seguir:
Método:
convertII()
Quando rodamos o caso de teste, vemos a barra vermelha porque o método convertII() falhou. O método convertI() continua verde, o que é bom.
Passo 7
Agora precisamos fazer com que a execução dos casos resultem na barra verde. Vamos implementar um método que satisfaça os dois casos. Podemos usar um simples if/else para cobrir ambos os casos, e no caso do else, lançamos IllegalArgumentException.
public int convertRomanToArabicNumber(String roman) {
if (roman.equals("I")) {
return 1;
} else if (roman.equals("II")) {
return 2;
}
throw new IllegalArgumentException();
}
Um ponto a evitar aqui é o lançamento de um NullPointerException
em um código como roman.equals("I"). Isso pode ser evitado simplesmente invertendo a igualdade para "I".equals(roman).
Executando os casos de teste novamente, devemos ver a barra verde para todos os casos.
Passo 8
Agora temos uma oportunidade para procurar por casos de refatoração, e tem código cheirando mau por aqui. Durante as refatorações, tipicamente procuramos por problemas como:
- Métodos longos;
- Código duplicado;
- Muitos if/else;
- Switch-case complexos;
- Simplificações de lógica e;
- Problemas de design.
O código mau cheiroso aqui (conseguiu encontrá-lo?) é o if/else com múltiplos retornos.
Talvez devamos refatorar o código introduzindo uma variável sum e um loop sobre os caracteres em romano. Se um caracter é I, adicionamos 1 a sum e então retornamos sum.
Mas amo a programação defensiva e por isso vou mover a cláusula throw para o else de forma a cobrir qualquer caractere inválido.
public int convertRomanToArabicNumber(String roman) {
int sum = 0;
for (char ch : roman.toCharArray()) {
if (ch == 'I') {
sum += 1;
} else {
throw new IllegalArgumentException();
}
}
return 0;
}
Do Java 10 em diante, podemos usar var
para definir a variável, usando var sum = 0 no lugar de int sum = 0;
Executamos os testes novamente para certificar de que a refatoração não alterou nenhuma funcionalidade.
Eita! A barra ficou vermelha. Ah! Todos os casos de teste retornaram 0 e erroneamente retornamos 0 ao invés da soma.
Se consertamos isso, agora temos um lindo verde.
Isso demonstra que não importa o quão trivial seja uma alteração, precisamos executar os testes depois de qualquer alteração. Sempre existe a possibilidade de introduzir bugs durante uma refatoração, e os casos de teste estão aqui para nos ajudar a encontrá-los. Isso mostra o poder dos testes de regressão.
Olhando para o código novamente, há outro problema. A exceção aqui não é descritiva, então devemos disponibilizar uma mensagem de erro significativa:
throw new IllegalArgumentException(String.format("Caracteres romanos ilegais %s", ch));
Executando os casos de teste novamente, devemos ver a barra verde para todos os casos.
Passo 9
Adicionamos outro caso de teste. Vamos converter III em 3.
Na mesma classe de teste, criamos um novo método de teste e a implementação:
Método:
convertIII()
Rode os casos de teste novamente e veremos a barra verde para todos. Nossa implementação já suporta este caso.
Passo 10
Agora precisamos converter V em 5.
Na mesma classe de testes, criamos um novo método de teste e a implementação:
Método:
convertV()
Executar os casos de teste leva à barra vermelha porque enquanto os outros testes passam, esse irá falhar.
Implemente essa funcionalidade adicionando um if/else ao if principal para verificar se o caractere é V e então some 5:
for (char ch : roman.toCharArray()) {
if (ch == 'I') {
sum += 1;
} else if (ch == 'V') {
sum += 5;
} else {
throw new IllegalArgumentException(String.format("Caracteres romanos ilegais %s", ch));
} }
Temos aqui uma oportunidade de refatoração mas não iremos efetuá-la neste passo. Na fase de implementação, nosso único objetivo é fazer o teste passar e nos mostrar uma barra verde. Não estamos dando atenção a design, refatoração ou ter um bom código. Quando o código passa no teste, podemos voltar a refatorá-lo.
Na fase de refatoração, focamos apenas na refatoração. Focar em uma coisa de cada vez evita distrações que possam reduzir a produtividade.
O caso de teste deveria ir para o estado verde.
Às vezes precisamos encadear comandos if/else. Como otimização, ordene as condições da mais comum à menos comum. Melhor ainda, se aplicável, é trocar por um switch-case para evitar fazer vários testes e cair diretamente no caso correto.
Passo 11
Temos um estado verde e estamos em fase de refatoração. Voltando ao método, podemos fazer algo sobre o if/else que não gosto.
Talvez, ao invés de usar if/else, possamos introduzir um Hashtable/HashMap para armazenar os algarismos romanos como chave e os equivalentes arábicos como valores.
Vamos remover o if e escrever sum += symbols.get(ch); no lugar. Adicione uma variável de instância.
private final Hashtable<Character, Integer> romanSymbols = new Hashtable<Character, Integer>() {
{
put('I', 1);
put('V', 5);
}
};
Precisamos verificar símbolos inválidos, por isso o código deve verificar se romanSymbols contém a chave e se não, lançar uma exceção.
public int convertRomanToArabicNumber(String roman) {
int sum = 0;
for (char ch : roman.toCharArray()) {
if (romanSymbols.containsKey(ch)) {
sum += romanSymbols.get(ch);
} else {
throw new IllegalArgumentException(
String.format("Caracteres romanos ilegais %s", ch));
}
}
return sum;
}
Execute o caso de teste que deve levar a uma barra verde.
Aqui temos outro ponto a ser verificado, mas em relação ao design, desempenho e código limpo. É melhor usar HashMap do que Hashtable porque o primeiro tem implementação assíncrona. Uma grande quantidade de chamadas a este método vai afetar o desempenho.
Uma dica de design é sempre usar uma interface, para deixar o código mais limpo e fácil de ser mantido. Fica fácil alterar a implementação de detalhes sem afetar o uso no código. No nosso caso, usaremos o Map.
private static Map<Character, Integer> romanSymbols = new HashMap<Character, Integer>() {
private static final long serialVersionUID = 1L;
{
put('I', 1);
put('V', 5);
}
};
No Java 9+, podemos substituir new HashMap<Character, Integer>() por new HashMap<>() já que o operador diamante funciona como classe interna anônima a partir do Java 9.
Ou podemos usar a forma mais simples Map.of()
Map<Character, Integer> romanSymbols = Map.of('I', 1, 'V', 5,'X', 10, 'L', 50,'C', 100, 'D', 500,'M', 1000);
As classes java.util.Vector e java.util.Hashtable estão obsoletas. Embora ainda tenham suporte, estão obsoletas desde o JDK 1.2 e não devem ser usadas em novos desenvolvimentos.
Após essa refatoração, precisamos verificar se está tudo bem e se não quebramos nada. Yay! o código está verde!
Passo 12
Vamos adicionar valores mais interessantes para o conversor. Voltando à nossa classe de testes, implementamos o teste para conversão de VI em 6.
Método:
convertVI()
Executamos o teste e vemos que o mesmo não dá erro. Parece que a lógica que implementamos já cobre esse caso. Ganhamos um case de graça novamente, sem necessidade de implementação.
Passo 13
Agora precisamos converter IV em 4 e isso não deve ser algo tão simples quanto converter VI em 6.
Método:
convertIV()
Executamos o caso de teste e vemos que resulta na barra vermelha como o esperado.
Precisamos fazer nosso teste passar. Reconhecemos que na representação romana um símbolo de valor menor (como um I) aparecendo antes de um símbolo de valor maior (como um V) reduz o valor numérico total - IV é igual a 4 enquanto VI é igual a 6.
Construímos um código que sempre soma os valores, mas para passar no teste, precisamos criar uma subtração. Precisamos de uma condição para a tomada da seguinte decisão: se o valor do símbolo anterior for maior ou igual ao do símbolo atual, faremos uma soma, senão faremos uma subtração.
É produtivo escrever a lógica que cobre o problema assim que vem à mente, sem nos preocuparmos com declaração de variáveis. Termine a lógica e então crie as variáveis que satisfaçam a implementação. Isso é o que chamamos de programar por intenção, como descrito anteriormente. Assim, sempre introduzimos o mínimo de código de forma mais rápida, sem que precisemos pensar tudo de antemão - o conceito de MVP.
Nossa implementação atual é:
public int convertRomanToArabicNumber (String roman) {
roman = roman.toUpperCase();
int sum = 0;
for (char chr : roman.toCharArray()) {
if (romanSymbols.containsKey(chr))
sum += romanSymbols.get(chr);
else
throw new IllegalArgumentException(
String.format("Caracteres romanos inválidos %s ", chr));
}
return sum;
}
Começamos nossa nova lógica para verificar a validade de algarismos romanos. Escrevemos a lógica normalmente e então criamos variáveis locais com a ajuda de dicas da IDE. Adicionalmente, temos a intuição de quais tipos de variáveis devem ser: se são locais ao método ou variáveis de instância ou classe.
int sum = 0, current = 0, previous = 0;
for (char chr : roman.toCharArray()) {
if (romanSymbols.containsKey(chr)) {
if (previous >= current) {
sum += current;
} else {
sum -= previous;
sum += (current - previous);
}
} else {
throw new IllegalArgumentException(
String.format("Caracteres romanos inválidos %s ", chr));
}
Agora precisamos alterar o laço para ser baseado em um índice e ter acesso à variável atual e a anterior, e atualizamos a implementação para satisfazer a nova mudança, de forma a compilar corretamente.
for (int index = 0; index < roman.length(); index++) {
if (romanSymbols.containsKey(roman.charAt(index))) {
current = romanSymbols.get(roman.charAt(index));
previous = index == 0 ? 0 : romanSymbols.get(roman.charAt(index-1));
if (previous >= current) {
sum += current;
} else {
sum -= previous;
sum += (current - previous);
}} else {
throw new IllegalArgumentException(
String.format("Caracteres romanos inválidos %s ", roman.charAt(index)));
}
Agora executamos os testes após a adição da nova funcionalidade e chegamos à barra verde. Perfeito.
Passo 14
No estado verde, estamos prontos para refatorar. Tentaremos uma refatoração mais interessante.
Nossa estratégia de refatoração sempre busca simplificar o código. Podemos ver que a linha romanSymbols.get(roman.charAt(index)) aparece duas vezes.
Extrairemos esse código duplicado para um método ou classe a ser usado aqui, para que futuramente as alterações sejam feitas em apenas um lugar.
Selecione o código, clique com o botão direito e escolha Netbeans refactoring tool > introduce > method. Nomeie-o getSymbolValue e deixe-o como um método privado, por fim, aperte OK.
Neste ponto, precisamos rodar os casos de teste, para ver que nossa pequena refatoração não introduziu nenhum erro no nosso código. Vemos que ainda estamos no verde.
Passo 15
Vamos fazer mais uma refatoração. A condição romanSymbols.containsKeyAt(index)) é de difícil leitura, não sendo fácil entender quando é válida ou não. Simplifiquemos esse código para que fique mais legível.
Embora saibamos o que essa linha faz, garanto que em seis meses será difícil entender o real motivo dela estar ali.
Legibilidade é uma das principais qualidades a ser melhorada sempre no TDD, porque quando trabalhamos com métodos ágeis alteramos o código de maneira frequente e rapidamente, e para fazer alterações rápidas, o código deve ser legível. E qualquer alteração também deve ser testável.
Iremos extrair essa linha de código em um método com o nome doesSymbolsContainsRomanCharacter, que descreve o que faz. E faremos isso com ajuda da ferramenta de refatoração do NetBeans, como fizemos no passo anterior.
Isso deixa nosso código mais fácil de ler. A condição é: se symbols contém um algarismo romano execute a rotina, senão lance uma IllegalArgumentException
Executamos novamente todos os testes e não encontramos erro nenhum.
Perceba que executamos os testes frequentemente, assim que introduzimos qualquer pequena refatoração. Não esperamos o fim de toda a refatoração que pretendemos fazer para só então executar os testes. Isso é essencial no TDD. Queremos feedback rápido e os casos de teste são nosso loop de feedback. Isso nos permite detectar qualquer erro assim que possível e em pequenos passos.
Como cada passo da refatoração pode potencialmente introduzir novos defeitos, quanto mais curto for o intervalo entre a alteração e o teste do código, mais rapidamente poderemos analisar o código e corrigir qualquer novo defeito.
Se refatorássemos cem linhas de código e então encontrassemos um erro ao executar os testes, precisaríamos passar algum tempo depurando o código para encontrar exatamente onde está o problema. Fica mais difícil encontrar a linha de código que introduziu o erro peneirando uma centena de linhas do que quando estamos lidando com apenas 5 a 10 linhas de alterações.
Passo 16
Temos duas linhas de código duplicadas dentro dos métodos privados que criamos. A linha duplicada é roman.charAt(index) e a vamos extrair para um novo método chamado getCharValue(String roman, int index) usando o NetBeans.
Executamos os testes novamente e tudo está verde.
Passo 17
Agora faremos alguma refatoração para otimizar nosso código e melhorar o desempenho. Podemos simplificar a lógica do cálculo do método de conversão que atualmente é:
int convertRomanToArabicNumber(String roman) {
roman = roman.toUpperCase();
int sum = 0, current = 0, previous = 0;
for (int index = 0; index < roman.length(); index++) {
if (doesSymbolsContainsRomanCharacter(roman, index)) {
current = getSymboleValue(roman, index);
previous = index == 0 ? 0 : getSymboleValue(roman, index - 1);
if (previous >= current) {
sum += current;
} else {
sum -= previous;
sum += (current - previous);
}
} else {
throw new IllegalArgumentException(
String.format("Caracteres romanos inválidos %s ",
getCharValue(roman, index)));
}
}
return sum;
}
Podemos economizar alguns ciclos da CPU melhorando essa linha:
previous = index == 0 ? 0 : getSymboleValue(roman, index - 1);
Não precisamos recuperar o caractere anterior dessa forma simplesmente porque é o que temos como caractere corrente no final de cada iteração. Podemos remover essa linha de código e adicionar previous == current; após o cálculo no fim do bloco do else.
Ao executar os testes deveríamos ver a barra verde.
Agora simplificaremos o cálculo para descartar outros ciclos de computação. Vou reverter o cálculo do if e for. O código final deve ser:
for (int index = roman.length() - 1; index >= 0; index--) {
if (doesSymbolsContainsRomanCharacter(roman, index)) {
current = getSymboleValue(roman, index);
if (current < previous) {
sum -= current;
} else {
sum += current;
}
previous = current;
} else {
throw new IllegalArgumentException(
String.format("Caracteres romanos inválidos %s ",
getCharValue(roman, index)));
}
Rode os casos de teste, que devem ficar verdes.
Como o método não altera qualquer estado do objeto, podemos torná-lo estático. Além disso, essa é uma classe utilitária, podendo ser fechada para herança. Uma vez que todos os métodos são estáticos, não deveríamos permitir que seja instanciada. Vamos fazer isso adicionando um construtor padrão privado e marcando a classe como final.
Agora temos um erro de compilação na classe de testes. Uma vez que seja corrigido, ao executar os testes deveríamos ver a barra verde novamente.
Step 18
O passo final é adicionar mais casos de teste para garantir que nosso código cubra todos os requisitos.
- Adicione o caso de testes convertX() que deve retornar 10, já que X = 10. Se executarmos o teste ele falhará com IllegalArgumentException até que adicionemos X = 10 no mapa de símbolos. Execute os testes novamente e neste caso irão passar. Nenhuma refatoração é necessária aqui.
- Adicione o caso de teste convertIX() que deve retornar 9, já que IX = 9. O teste passará.
- Adicione ao mapa de símbolos os valores: L = 50, C = 100, D = 500, M = 1000.
- Acidione o caso de teste convertXXXVI() que deve retornar 36, já que XXXVI = 36. Rode os testes e devem passar. Nenhuma refatoração é necessária.
- Adicione o caso de teste convertMMXII() que deve retornar 2012. Execute os testes e devem passar. Nenhuma refatoração é necessária.
- Adicione o caso de teste convertMCMXCVI() que deve retornar 1996. Rode os testes e devem passar. Nenhuma refatoração é necessária.
- Adicione o teste convertInvalidRomanNumber() que deve lançar uma exceção do tipo IllegalArgumentException. Rode os testes e passarão. Nenhuma refatoração é necessária.
- Adicione o caso de teste convertVII() que deve retornar 7, já que VII = 7. Mas quando tentamos usar vii como entrada (com letras minúsculas) o teste falha, lançando a exceção IllegalArgumentException porque só tratamos letras maiúsculas. Para consertar isso, adicionamos a linha roman = roman.toUpperCase(); no início do método. Execute os testes novamente e a barra ficará verde. Nenhuma refatoração é necessária.
Neste ponto, concluímos nossa obra de arte. Com o TDD em mente, precisamos de alterações mínimas no código fonte para que todos os testes passem e atendam todos os requisitos e com refatoração garantimos que o código tem boa qualidade (tanto de desempenho, quanto legibilidade e design).
Espero que tenha gostado, e que isso te encoraje a começar a usar o TDD no próximo projeto ou até na tarefa que esteja executando atualmente. Por favor ajude a difundir essa mensagem compartilhando ou fazendo referência a esse artigo, ou ainda adicionando uma estrela ao código fonte no GitHub.
Sobre o autor
Mohamed Taman é arquiteto corporativo sênior da @Comtrade digital services, Java Champion, Embaixador Oracle Groundbreaker, Adopt Java SE.next(), JakartaEE.next(), membro do JCP, foi membro do Comitê Executivo do JCP, JSR 354, 363 e 373 e membro do grupo de especialistas, líder da EGJUG, membro do conselho do Oracle Egypt Architects Club, adora Mobile, Big Data, Cloud, Blockchain, DevOps. Palestrante internacional, e autor de livros e vídeos de "JavaFX essentials", "Getting Started with Clean Code, Java SE 9" e "Hands-On Java 10 Programming with JShell" e do novo livro "Secrets of a Java Champions". Ganhou a escolha de Duke de 2015, 2014 e o excelente prêmio de participante adopt-a-jar de 2013 da JCP.