Anotações Capítulo 9: Unit Tests
Jonilson Sousa
Posted on July 26, 2021
Antigamente os testes era um programa simples de controle que interagia manualmente com o programa escrito.
Exemplo: Um simples programa de controle que esperava alguma ação no teclado. Ele só iria agendar um o caractere digitado para reproduzir 5 segundos depois. Então digitava uma melodia no teclado e esperava que fosse reproduzida na tela depois. Esse era o teste.
Geralmente o código de teste era jogado fora.
Mas atualmente deveríamos criar um teste que garantisse que cada canto do código funcionasse como esperamos.
Isolarmos o código do resto do sistema operacional em vez de invocar as funções padrões de contagem.
Simularmos aquelas funções de modo que tivéssemos o controle absoluto sobre o tempo.
Agendaremos comandos que configuraram flags booleanas; Adiantamos o tempo, observando as flags e verificando se elas mudam de falsas para verdadeiras quando colocamos o valor correto no tempo.
E para passar uma coleção de testes adiante, precisamos nos certificar se eles são adequados para serem executados por qualquer pessoa e se eles e o código estão juntos no mesmo pacote.
Progredimos bastante ao longo do tempo, com os movimentos Agile e TDD. Porém muitos programadores têm se esquecido de alguns dos pontos mais sutis e importantes de escrever bons testes.
As três leis do TDD
Sabemos que o TDD nos pede para criar primeiro os testes e depois o código de produção. Mas isso é apenas o início. Considere as três leis abaixo:
Primeira Lei: Não se deve escrever o código de produção até criar um teste de unidade de falhas.
Segunda Lei: Não se deve escrever mais de um teste de unidade do que o necessário para falhar, e não compilar é falhar.
Terceira Lei: Não se deve escrever mais códigos de produção do que o necessário para aplicar o teste de falha atual.
Os testes e o código de produção são escritos juntos, com os testes apenas alguns segundos mais adiantados.
Dessa forma teríamos milhares de testes, e os mesmos cobririam praticamente todo o nosso código de produção. Assim o tamanho completo dos testes, pode competir com o tamanho do próprio código de produção, o que pode apresentar um problema de gerenciamento intimidador.
Como manter os testes limpos
Testes feitos “porcamente” é o equivalente, se não pior, a não ter teste algum.
Já que muitos testes devem ser alterados conforme o código de produção evolui.
Quanto pior o teste, mais difícil é mudá-lo.
Quanto mais confuso o teste, maiores as chances de levar mais tempo escrevendo novos testes.
E conforme o código de produção é alterado, os testes antigos começam a falhar e a bagunça nos testes dificulta fazê-los funcionar novamente.
Assim, os testes passam a ser vistos como um problema em constante crescimento.
Com tudo, a manutenção dos testes pode contribuir negativamente para a finalização do projeto. E no final podemos ser forçados a descartar todos os testes.
Porém sem os testes, não podemos garantir que, após as alterações no código, ele funcionasse como o esperado e que mudanças em uma parte do sistema não afetam outras partes.
Com isso, os bugs crescem, e o medo de alterar o código fica maior. E com isso o código de produção começa a se degradar.
No fim de tudo, ficamos sem testes, com um código de produção confuso e cheio de bugs, com consumidores frustrados e o esforço para criação de testes não valeu de nada.
Tudo isso é fato, porém se tivéssemos mantidos os testes limpos, o esforço para a criação dos testes não teria sido em vão.
Assim: os código de testes são tão importantes quanto o código de produção. Ele não é secundário, ele requer raciocínio, planejamento e cuidado. É Preciso mantê-lo tão limpo quanto o código de produção.
Os testes habilitam as “-idades”
Se não mantiver seus testes limpos, irá perdê-los.
E sem eles, perdemos exatamente o que mantém a flexibilidade no código de produção.
São os testes de unidade que mantêm seus códigos flexíveis, reutilizáveis e passíveis de manutenção. Isso porque se tiver testes, não terá medo de alterar o código!
Quanto maior a cobertura de seus testes, menor o medo.
Portanto, ter uma coleção de testes de unidade automatizados que cubram o código de produção é o segredo para manter seu projeto e arquitetura o mais limpos possíveis.
Os testes habilitam todas as “-idades”, pois eles possibilitam as alterações.
Do contrário, se seus testes forem ruins, sua capacidade de modificar seu código fica comprometida e perde a capacidade de alterá-lo. No fim, você perde os testes e seu código se degrada.
Testes limpos
O que torna um teste limpo? Três coisas: Legibilidade, legibilidade e legibilidade. Talvez isso seja até mais importante nos testes de unidade do que no código de produção.
O que torna os testes legíveis? O mesmo que torna os códigos legíveis: Clareza, simplicidade, consistência de expressão.
Considere o código do FitNesse:
public void testGetPageHierarchyAsXml() throws Exception {
crawler.addPage(root, PathParser.parse("PageOne"));
crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root, PathParser.parse("PageTwo"));
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
assertSubString("<name>PageTwo</name>", xml);
assertSubString("<name>ChildOne</name>", xml);
}
public void testGetPageHierarchyAsXmlDoesntContainSymbolicLinks() throws Exception {
WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));
crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root, PathParser.parse("PageTwo"));
PageData data = pageOne.getData();
WikiPageProperties properties = data.getProperties();
WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);
symLinks.set("SymPage", "PageTwo");
pageOne.commit(data);
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
assertSubString("<name>PageTwo</name>", xml);
assertSubString("<name>ChildOne</name>", xml);
assertNotSubString("SymPage", xml);
}
public void testGetDataAsHtml() throws Exception {
crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");
request.setResource("TestPageOne");
request.addInput("type", "data");
Responder responder = new SerializedPageResponder();
SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("test page", xml);
assertSubString("<Test", xml);
}
Veja as chamadas para
PathParser
. Elas transformam strings em instânciasPagePath
peloscrawler
. Essa transformação é irrelevante na execução do teste e serve apenas para ofuscar o objetivo.Os detalhes em torno da criação do responder e a definição do tipo de
response
também são apenas aglomerados. E em nada interferem na forma pela qual é construído a URL requisitada a partir de umresource
e um parâmetro.No final, esse código não foi feito para ser lido. Tem muitos detalhes para serem entendidos antes. Agora considere os mesmos testes refatorados:
public void testGetPageHierarchyAsXml() throws Exception {
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
submitRequest("root", "type:pages");
assertResponseIsXML();
assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}
public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {
WikiPage page = makePage("PageOne");
makePages("PageOne.ChildOne", "PageTwo");
addLinkTo(page, "PageTwo", "SymPage");
submitRequest("root", "type:pages");
assertResponseIsXML();
assertResponseContains( "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
assertResponseDoesNotContain("SymPage");
}
public void testGetDataAsXml() throws Exception {
makePageWithContent("TestPageOne", "test page");
submitRequest("TestPageOne", "type:data");
assertResponseIsXML();
assertResponseContains("test page", "<Test");
}
A estrutura tornou óbvio o padrão CONSTRUIR-OPERAR-VERIFICAR.
Cada teste está dividido em três partes: Primeira produzir os dados do teste, segunda operar sobre eles, terceira verifica os resultados gerados.
E a grande maioria dos dados desnecessários foi eliminada.
Os testes vão direto ao ponto e usam apenas os tipos de dados e funções que realmente precisam.
Assim quem ler pode rapidamente descobrir o que eles fazem. Sem ser prejudicado pelos detalhes.
Linguagem de testes específica do domínio
O exemplo anterior mostra a técnica da construção de uma linguagem específica a um domínio.
Em vez de usar as APIs que os programadores utilizam para manipular o sistema, construímos uma série de funções e utilitários, que tornam os testes mais fáceis de escrever e ler.
Assim, desenvolvedores disciplinados também refatoram seus códigos de teste para formas mais sucintas e expressivas.
Um padrão duplo
Os códigos de testes são totalmente diferentes do código de produção. Ele pode ser simples, sucinto, expressivo, mas não precisa ser mais eficiente do que o código de produção.
Ele roda num ambiente de teste, e não de um de produção, e esses dois ambientes possuem requisitos diferentes.
Considere o código abaixo, sem entrar em detalhes posso lhe dizer que o teste verifica se o alarme de temperatura baixa, o aquecedor e o ventilador estão todos ligados quando a temperatura estiver “fria demais”:
@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
hw.setTemp(WAY_TOO_COLD);
controller.tic();
assertTrue(hw.heaterState());
assertTrue(hw.blowerState());
assertFalse(hw.coolerState());
assertFalse(hw.hiTempAlarm());
assertTrue(hw.loTempAlarm());
}
Sem se preocupar com os detalhes, apenas focando no estado final do sistema quando a temperatura é “fria demais”.
E notamos que sempre precisamos ver o que está sendo verificado e voltarmos para a esquerda para esquerda para ver se é
assertTrue
ouassertFalse
. Isso é falível, além de dificultar a leitura do teste.Agora vejamos a refatoração do teste aperfeiçoando a legibilidade:
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals("HBchL", hw.getState());
}
Foi criado uma função
wayTooCold()
para ocultar o detalhe da funçãotic
.Mas o estranho é a string em
assertEquals
. As letras maiúsculas significam “ligado” e as minúsculas “desligado”, e elas sempre seguem a ordem:{heater, blower, cooler, hi-temp-alarm, lo-temp-alarm}
.“Mesmo que esse código esteja perto de violar a regra de mapeamento mental, neste caso parece apropriado”.
Assim, conhecendo o significado da string, logo conseguimos interpretar os resultados. Assim, veja a listagem de testes:
@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
tooHot();
assertEquals("hBChl", hw.getState());
}
@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
tooCold();
assertEquals("HBchl", hw.getState());
}
@Test
public void turnOnHiTempAlarmAtThreshold() throws Exception {
wayTooHot();
assertEquals("hBCHl", hw.getState());
}
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals("HBchL", hw.getState());
}
- Agora veja o código da função
getState()
:
public String getState() {
String state = "";
state += heater ? "H" : "h";
state += blower ? "B" : "b";
state += cooler ? "C" : "c";
state += hiTempAlarm ? "H" : "h";
state += loTempAlarm ? "L" : "l";
return state;
}
Esse código não é muito eficiente. Para isso deveria ser usado o StringBuffer
.
As
StringBuffer
são um pouco feias. Mas evitamos usar no código de produção se o custo for pequeno.Contudo, esse aplicativo é um sistema em tempo real, e é provável que os recursos sejam limitados. Entretanto, o ambiente de testes não costuma ter limitação.
“Essa é a natureza do padrão duplo. Há coisas que você talvez jamais faça num ambiente de produção que esteja perfeitamente bem em um ambiente de teste. Geralmente, isso envolve questões de eficiência de memória e da CPU. Mas nunca de clareza.”
Uma afirmação por teste
Há uma escola de pensamento que diz que cada teste em JUnit deve ter uma e apenas uma instrução de afirmação (assert). Essa regra pode parecer perversa, mas a vantagem pode ser vista no exemplo da listagem de testes do aplicativo em tempo real, cujo os testes chegam a uma única conclusão que é fácil e rápida de entender.
Mas na listagem dos testes de URL, parece ilógico que poderíamos de alguma forma adicionar afirmação se a saída está em XML e se ela contém certas
substrings
. Para resolver isso poderíamos dividir o teste em dois, cada um com sua afirmação:
public void testGetPageHierarchyAsXml() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldBeXML();
}
public void testGetPageHierarchyHasRightTags() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldContain("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}
Note que os nomes das funções para usar a convenção comum dado-quando-então. Isso facilita ainda mais a leitura dos testes. Infelizmente, dividir os testes pode gerar muito código duplicado.
Podemos usar o padrão Template Method para eliminar a duplicação e colocar as partes dado/quando na classe base e as partes então em derivadas diferentes.
Ou poderíamos criar uma classe de teste completamente separada e colocar as partes dado e quando na função
@Before
e as partes quando em cada função@Test
.Mas em ambos os casos parece muito trabalho, no fim é preferível usar múltiplas afirmações.
A regra da afirmação única é uma boa orientação. E geralmente podemos tentar criar uma linguagem de teste para um domínio específico que a use. Mas não tem problema em colocar mais de uma afirmação em um teste.
Um único conceito por teste
Talvez a melhor regra seja que desejamos um único conceito em cada função de teste.
Não queremos funções longas que saiam testando várias coisas uma após a outra.
No exemplo abaixo o teste é longo e sai testando várias coisas. Ele deveria ser dividido em três:
/**
* Miscellaneous tests for the addMonths() method.
*/
public void testAddMonths() {
SerialDate d1 = SerialDate.createInstance(31, 5, 2004);
SerialDate d2 = SerialDate.addMonths(1, d1);
assertEquals(30, d2.getDayOfMonth());
assertEquals(6, d2.getMonth());
assertEquals(2004, d2.getYYYY());
SerialDate d3 = SerialDate.addMonths(2, d1);
assertEquals(31, d3.getDayOfMonth());
assertEquals(7, d3.getMonth());
assertEquals(2004, d3.getYYYY());
SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
assertEquals(30, d4.getDayOfMonth());
assertEquals(7, d4.getMonth());
assertEquals(2004, d4.getYYYY());
}
- As funções de teste devem ser:
- Dado o último dia de um mês com 31 dias (como maio):
- Quando você adicionar um mês cujo último dia seja 30 (como junho), então deverá ser o dia 30 daquele mês, e não o 31.
- Quando você adicionar dois meses aquela data cujo último dia seja 31, então a data deverá ser o dia 31.
-
Dado o último dia de um mês com 30 dias (como junho):
- Quando você adicionar um mês cujo último dia seja 31, então a data deverá ser dia 30 e não 31.
- Ainda está faltando o teste que verifica que ao incrementar o mês, a data não pode ser maior do que o último dia daqueles mês.
- Assim, as múltiplas afirmações não são problema e sim os conceitos, que nesse caso é mais de um.
- Assim, é melhor minimizar o número de afirmações por conceito e testar apenas um conceito por teste.
F.I.R.S.T
Testes limpos seguem outras cinco regras:
Rapidez (Fast): Os testes devem ser rápidos.
Independência (Independent): Os testes não devem depender uns dos outros.
Repetitividade (Repeatable): Deve-se poder repetir os testes em qualquer ambiente.
Autoavaliação (Self-Validating): Os testes devem ter uma saída booleana. Obtenham ou não êxito, você não deve ler um arquivo de registro para saber o resultado.
Pontualidade (Timely): Os testes precisam ser escritos em tempo hábil. Devem-se criar os testes de unidade imediatamente antes do código de produção no qual serão aplicados.
Conclusão
Este é só o início deste tópico.
Deveria ter um livro inteiro sobre testes limpos.
Os testes são tão importantes para a saúde de um projeto quanto o código de produção.
Talvez até mais, pois eles preservam e aumentam a flexibilidade, capacidade de manutenção e reutilização do código de produção.
Mantenha seus testes sempre limpos.
Se deixar os testes se degradarem, seu código também irá. Mantenha limpos seus testes.
Posted on July 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.