Golden Master Testing: Testando Código Legado
A insegurança de tocar em um código legado com grande valor para a aplicação é um trabalho de muita tensão, mas podemos aliviar essa tensão com a técnica de teste correta.
Tudo bem, pessoal? No último artigo aqui no Blog nós falamos sobre o Code Kata chamado Gilded Rose, um desafio onde temos um código ruim e precisamos adicionar uma nova funcionalidade, e algumas pessoas demonstraram interesse em saber como eu fiz para implementar a funcionalidade que o desafio pedia.
Durante o desafio eu tive algumas dificuldades e encontrei algumas soluções muito mais práticas relacionada aos testes, então vim compartilhar com vocês esse conhecimento.
Mas o que exatamente eu estava encarando? Vamos dar uma relembrada.
O problema
O código que tínhamos na classe principal, GildedRose
, é um completo caos. É o tipo de código que eu fazia quando ainda estava começando a programar. Podemos resumir esse caos em uma grande função chamada updateQuality
que possui mais de uma responsabilidade, com várias condicionais aninhadas e para piorar elas são negativas (!
not). Se eu fosse um psicopata é bem provável que eu estaria procurando o endereço do antigo programador para lhe dar os parabéns por esse belo código. :p
Eu precisava adicionar uma nova funcionalidade nesse método updateQuality
, mas por onde eu devia começar? Eu deveria refatorar as condicionais? Os códigos repetidos? Tudo parecia muito ruim, então o que eu devia fazer? Eu estava com tanta vontade de começar a mexer no código que acabei esquecendo qual é o passo mais importante ao lidar com código legado, que é garantir que não vamos quebrar nada! O mais importante é maximar a segurança ao refatorar o código e para garantir isso nós precisamos utilizar os testes.
Pensando nisso eu comecei a implementar MUITOS TESTES:
@Test
public void standardItemQualityShouldNotBeNegative() { ... }
@Test
public void sellInShouldDecrementBelowZero() { ... }
@Test
public void standardItemShouldDecrementSellIn() { ... }
@Test
public void standardItemShouldDecrementQuality() { ... }
@Test
public void standardItemShouldDecrementQualityTwiceWhenSellInHasPassed() { ... }
@Test
public void agedBrieShouldDecrementSellIn() { ... }
@Test
public void agedBrieItemShouldIncrementQuality() { ... }
@Test
public void agedBrieItemShouldNotIncrementQualityOverFifty() { ... }
@Test
public void agedBrieItemShouldIncrementQualityByTwiceWhenSellInHasPassed() { ... }
@Test
public void legendaryItemShouldBeImmutable() { ... }
@Test
public void backstagePassesShouldDecrenentSellIn() { ... }
@Test
public void backstagePassesShouldIncrementQuality() { ... }
@Test
public void backstagePassesShouldIncrementQualityByTwoWhenSellInIsBelowTen() { ... }
@Test
public void backstagePassesShouldIncrementQualityByThreeWhenSellInIsBelowFive() { ... }
@Test
public void backstagePassesShouldDropQualityToZeroWhenSellInHasPassed() { ... }
Eu estava escrevendo testes para todos os cenários que eu podia imaginar. E mesmo dedicando tanto esforço para pensar em todas cenários possíveis, mas independente de tanto esforço eu fui incapaz de criar testes que protegessem a aplicação de quebrar ao refatorar o código. Por que isso aconteceu?
Bem, meu primeiro erro foi confiar na especificação de requisitos que me foi passado. Eu não considerei que os requisitos pudessem estar desatualizados ou que estivessem com informações incorretas. Então os requisitos nesse caso não eram realmente confiáveis e eu não tinha como saber disso.
O meu segundo erro foi tentar prever todas as possibilidades. Existiam muitas possibilidades, e tentar adivinhar todas elas
Imagine que você está jogando cara ou coroa com apenas uma moeda. Se jogarmos apenas uma moeda teremos apenas dois possíveis resultados: cara ou coroa. Mas se jogarmos duas moedas? Dessa vez nós teremos o dobro de resultados: cara e cara, ou cara e coroa, ou coroa e cara, ou coroa e coroa. Quando aumentamos a quantidade de moedas que jogamos as combinações dobram. É um aumento exponencial.
Esse mesmo comportamento pode acontecer ao adicionarmos novos parâmetros ou condicionais para um método.
Cada condicional possui a possibilidade de ser verdadeiro ou falso (o cara ou coroa da nossa moeda). Olhe o exemplo:
if( condicao1 ) { System.out.println("Hello");}
if (condicao2) { System.out.println("World");}
System.out.println("!!!");
A saída desse código pode ser: HelloWorld!!!, ou Hello!!!, ou World!!!, ou !!!.
Então se nosso código tiver condicionais nós vamos ter que testar os cenários onde elas são verdadeiro e os cenários onde elas são falso. É quase a mesma coisa que acontece com as moedas, onde para cada nova moeda que lançamos nós dobramos as combinações possíveis.
Mas então como podemos testar esse código de forma eficiente sem precisar prever todas as possibilidades possíveis? Comecei a quebrar a cabeça durante alguns minutos e pesquisa sobre algumas técnicas de testes e acabei descobrindo algo novo.
Golden Master Testing
Acabei descobrindo o Golden Master Testing. O nome Golden Master ou Gold Master é como algumas pessoas chamam o código da aplicação que está em produção, então Golden Master Testing nada mais é do que uma técnica utilizada para testar código legado que está em produção. Esse técnica também é conhecida como Characterization Test. (Golden Master Testing soa muito mais cool que Characterization Test. :p)
O objetivo desse técnica é maximizar a segurança ao refatorar a aplicação, pois com ele nós conseguimos extrair o comportamento atual da aplicação de um pedaço de código sem depender cegamente da especificação e sem precisar prever o comportamento do código nos testes. E como essa técnica faz essa bruxaria?
É muito simples. Nós vamos invocar o método algumas milhares de vezes gerando aleatoriamente utilizando uma semente (um valor que será tomado com base para gerar os números aleatórios) para cada uma das entradas possíveis que o método pode receber e então vamos salvar o resultado de cada uma das chamadas do método em um arquivo de texto! Esse arquivo de texto será no master.txt e ele terá uma amostragem dos resultados possíveis do nosso método.
Das próximas vezes que rodarmos o teste, nós vamos gerar fazer o mesmo processo utilizando a mesma semente, vamos salvar os resultados do método em um arquivo chamado test-run.txt e então comparamos com o arquivo que criamos antes chamado master.txt. Se ambos os arquivos estiverem idênticos, significa que nossas alterações não quebraram o código, então nós garantimos que é seguro continuar refatorando o código. :)
A implementação do Golden Master Testing no GildedRose
Vamos então ver como ficaria a implementação dessa técnica no GildedRose, mas você pode aplicar também nas aplicações que você estiver começando a trabalhar seguindo essa mesma metodologia.
Primeiro nós precisamos saber quais entradas nosso método pode receber e no caso do GildedRose o método updateQuality
modifica um objeto Item
e o construtor de Item
é composto por um nome (name
), uma data para ser vendido (sellIn
) e a qualidade dele (quality
).
Além disso nós precisamos consultar o código do método e ver a especificação para saber as possíveis entradas que o método pode receber, e nesse caso são:
- O atributo
name
pode receber as entradasAged Brie
,Backstage passes to a TAFKAL80ETC concert
,Sulfuras, Hand of Ragnaros
eItem com nome qualquer
que não possui nenhuma regra específica. - O atributo
sellIn
pode receber qualquer número inteiro positivo ou negativo. - O atributo
quality
não pode ter um número acima de 50 e abaixo de 0.
Vamos então definir algumas constantes e atriutos na classe GildedRoseTest
para deixar claro o comportamento que será testado:
public static final int SEMENTE_FIXA = 1;
public static final int NUMERO_DE_ITENS = 5000;
public static final String ARQUIVO_MASTER = "master.txt";
public static final String ARQUIVO_TESTE_ATUAL = "test-run.txt";
public static final String[] NOME_DOS_ITENS = { "Nome Qualquer", "Aged Brie",
"Backstage passes to a TAFKAL80ETC concert", "Sulfuras, Hand of Ragnaros" };
public static final int MAX_SELLIN = 10;
public static final int MIN_SELLIN = -10;
public static final int MAX_QUALITY = 50;
public static final int MIN_QUALITY = 0;
private Random random = new Random(SEMENTE_FIXA);
Essas constantes possuem o seguinte objetivo:
SEMENTE_FIXA
será utilizado para gerar os números aleatórios.NUMERO_DE_ITENS
será a quantidade de vezes que meu método irá rodar, que nesse caso é baseado no número de itens.ARQUIVO_MASTER
eARQUIVO_TESTE_ATUAL
são os nomes dos arquivos que serão criados.NOME_DOS_ITENS
é um Array com todas possíveis entradas do atributoname
.MAX_SELLIN
eMIN_SELLIN
serão os valores entre os quais poderão ser passados para o atributosellIn
.MAX_QUALITY
eMIN_QUALITY
serão os valores entre os quais poderão ser passados para o atributoquality
.- O atributo
random
que irá gerar nossos números aleatórios utilizando aSEMENTE_FIXA
que criamos.
A classe
Random
recebe um inteiro como parâmetro do construtor que é chamada de Seed (Semente). Essa semente vai garantir que o métodos comonextInt()
retornem sempre os mesmo valores aleatórios baseado na semente que foi utilizada.
Agora que temos nossas constantes, vamos criar um método chamado gerarItensAleatorio
que retorna um Array de Item
s com os atributos aleatórios, dessa forma:
private Item[] gerarItensAleatorio(int numeroDeItens) {
Item[] items = new Item[numeroDeItens];
for (int i = 0; i < numeroDeItens; i++) {
items[i] = getItemAleatorio();
}
return items;
}
private Item getItemAleatorio() {
return new Item(getNameAleatorio(), getSellInAleatorio(), getQualityAleatorio());
}
private String getNameAleatorio() {
return NOME_DOS_ITENS[random.nextInt(NOME_DOS_ITENS.length)];
}
private int getSellInAleatorio() {
return gerarNumeroAleatorioEntre(MIN_SELLIN, MAX_SELLIN);
}
private int getQualityAleatorio() {
return gerarNumeroAleatorioEntre(MIN_QUALITY, MAX_QUALITY);
}
private int gerarNumeroAleatorioEntre(int minimo, int maximo) {
return random.nextInt(maximo - minimo) + minimo;
}
E agora vamos criar um método chamado gerarAmostragemDeTeste
que vai criar nossa amostragem. Ele vai gerar o caminhoDoArquivo
, que será uma das constantes que criamos anteriormente (ARQUIVO_MASTER
ou ARQUIVO_TESTE_ATUAL
) e também a quantidade de itens que ele deve gerar, que também será a constante que criamos anteriormente (NUMERO_DE_ITENS
).
private void gerarAmostragemDeTeste(Path caminhoDoArquivo, int numeroDeItens) throws IOException {
Item[] itensAleatorios = gerarItensAleatorio(numeroDeItens);
gildedRoseApp = new GildedRose(itensAleatorios);
gildedRoseApp.updateQuality()
try (BufferedWriter arquivoParaEscrever = Files.newBufferedWriter(caminhoDoArquivo)) {
for (Item item : itensAleatorios) {
arquivoParaEscrever.write(item.toString() + "\n");
}
}
}
Esse método irá gerar pegar os itens aleatórios que geramos, instanciar a nossa aplicação GildedRose
e chamar o método updateQuality
que aplica a regra de negócio. Depois disso nós criamos um arquivo e salvamos o resultado dos itens.
Para gerarmos o master.txt vamos criar o método main
na nossa classe de teste dessa forma:
public static void main(String[] args) throws IOException {
GildedRoseTest grt = new GildedRoseTest();
grt.gerarAmostragemDeTeste(ARQUIVO_MASTER, NUMERO_DE_ITENS);
}
Sempre que rodarmos essa classe como uma Java Application, ela irá criar um novo master.txt.
E para testar nosso código, vamos criar um método chamado comparaTesteAtualComMaster
com a anotação @Test
dessa forma:
@Test public void comparaTesteAtualComMaster() throws IOException {
gerarAmostragemDeTeste(ARQUIVO_TESTE_ATUAL, NUMERO_DE_ITENS);
String arquivoDoTesteAtual = lerArquivo(ARQUIVO_TESTE_ATUAL);
String arquivoDoMaster = lerArquivo(ARQUIVO_MASTER);
assertEquals(arquivoDoTesteAtual, arquivoDoMaster);
}
private String lerArquivo(Path caminhoDoArquivo) throws UnsupportedEncodingException, IOException {
return new String(Files.readAllBytes(caminhoDoArquivo), "UTF-8");
}
Dessa forma quando rodarmos o teste com o JUnit ele irá criar o nosso ARQUIVO_TESTE_ATUAL
(test-run.txt), abrir ambos os arquivos na memória e compará-los dentro do assertEquals
. Caso ambos sejam iguais, significa que estamos seguros para continuar refatorando o código da classe principal (GildedRose
).
Você deve rodar a classe
GildedRoseTest
comoJava Application
apenas uma vez para gerar o arquivo master.txt. Tome cuidado para não gerar acidentalmente um novo master.txt quando sua aplicação estiver quebrando pois isso fará com que o teste tenha salvo o comportamento incorreto da aplicação.
Quando o teste falhar como na imagem abaixo, é possível visualizar a diferença entre os arquivos utilizando um recurso da IDE.
Para isso você deve clicar com o direito sobre o Failure trace da direita como o da imagem e clicar em Compare Result.
Assim ficará muito mais fácil visualizar onde os erros ocorreram.
O código completo da implementação encontra-se neste link
Conclusão
Nesse artigo nós vimos sobre uma das metodologias que podem ser utilizadas para testar código legado. Essa metodologia não possui muita utilidade em código que já possui uma boa bateria de testes e sua utilização é exclusiva para eliminar o trabalho de implementar teste por teste na aplicação.
A técnica Golden Master Testing é ótima para lidar com métodos complexos que recebem muitas entradas e que possui um comportamento difícil de seguir, além de ser extremamente fácil de implementar, mas ter que lidar com dois arquivos (master.txt e test-run.txt) pode causar problemas e é necessário tomar cuidado principalmente ao gerar o arquivo do master.txt para que você não deixe um erro passar despercebido pois essa técnica não garante que seu método está correto e sim que o seu método não produziu nenhum efeito colateral indesejado.
Michael Feathers escreveu em seu livro , Working Effectively with Legacy Code que código legado é aquele código que não possui testes unitários. O código legado possui partes importantes do software, mas a falta de testes nos tira a confiança para alterar melhorar o código. Utilizar testes é uma habilidade indispensável para os programadores, seja utilizando a metodologia do Test-Driven Development ou apenas aplicando testes unitários depois do código pronto. O importante é utilizar.
Então se você estiver tendo dificuldades para colocar em prática o Test-Driven Development ou os testes unitários, entra no site da TriadWorks e dê uma boa olhada no Curso TDD e Testes Automatizados com Java. Com certeza será uma habilidade valiosa para sua carreira como programador.
E vocês conhecem outras técnicas interessantes para testar código legado? Comentem aqui embaixo contando para gente. Comente também se você tiver qualquer crítica ou sugestão. :)
You might also be interested in these articles...
Desenvolvedor na TriadWorks - Email
Posted in: bugsclean codecurly lawgolden mastergolden master testjunitrefactoringtddtestestestes automatizadostestes manuaistesting