Lidando com testes mentirosos - Show me the code

Skinner Em lidando com testes mentirosos e lidando com testes mentirosos - estabilizando os ofensores falei sobre algumas estratégias para lidar com testes que falham mesmo quando não existem problemas com a aplicação. Nesse post vamos ver como essas estratégias funcionam na prática.

Para demonstrar esses conceitos vamos fazer um sistema a pedido do Sr. Skinner. Em sua escola, mais uma vez está acontecendo a esperada feira de ciências. Ela contará com alguns personagens conhecidos como Bart, Lisa e o relegado Milhouse.

Para facilitar a eleição do campeão e tornar o processo mais transparente e justo o Sr. Skinner pediu um sistema onde a banca avaliadora irá cadastrar os projetos com suas respectivas notas e o sistema irá informar quem é o campeão da rodada.

Os critérios avaliados, com seus respectivos pesos, são:

  • O conhecimento do aluno. Peso 3.
  • Apresentação do projeto. Peso 2,
  • Criatividade. Peso 1,
  • Se é amigo do Skinner. Se o aluno for amigo do Skinner ele sempre será campeão (eu comentei que o sistema era para tornar o processo mais justo?).

O sistema será composto por duas telas, uma onde os participantes serão cadastrados e outra que exibirá o campeão até o momento. Vamos começar com a mecânica de cálculo de pontuação, que será feita pela classe Contestant (participante). Como bons devenvolvedores ágeis vamos desenvolver o sistema com TDD. Ele será feito em Java com Wicket, Hibernate e Spring.

Testes unitários

Testes unitários

Vamos supor que Lisa ganhe 10 em conhecimento. Por causa do peso, sua pontuação deve ser 30.

Contestant lisa = new Contestant( "Lisa" );  
lisa.setKnowlegdeScore( 10.00 );  
assertEquals(30.0, lisa.getScore());  

Vamos usar a mesma lógica para os outros critérios:

lisa.setPresentationScore( 10.00 );  
assertEquals(20.0, lisa.getScore());  
lisa.setCreativity( 10.00 );  
assertEquals(10.0, lisa.getScore());  

Dado que já sabemos calcular a pontuação, vamos para a escolha de um vencedor. A escolha de um vencedor será feita pela classe ScienceFair (feira de ciências). Dado que lisa tenha nota 10 nos três critérios e Bart 0 (uma situação bem próxima à realidade), Lisa deve ser a vencedora.

Contestant bart = new Contestant( "Bart" );  
bart.setKnowlegdeScore(0.0);  
bart.setPresentationScore(0.0);  
bart.setCreativity(0.0);

Contestant lisa = new Contestant( "Lisa" );  
lisa.setCreativity(10.0);  
lisa.setKnowlegdeScore(10.0);  
lisa.setPresentationScore(10.0);

scienceFair.addContestant( bart );  
scienceFair.addContestant( lisa );

assertEquals( lisa, scienceFair.getWinner() );  

Agora entra nosso critério obscuro. Se o participante não for amigo do Skinner ele não poderá ganhar. Supondo que Milhouse seja amigo do Skinner, mesmo com sua nota pífia ele ganha de Lisa.

Contestant milhouse = new Contestant( "Milhouse" );  
milhouse.setKnowlegdeScore(2.0);  
milhouse.setPresentationScore(2.0);  
milhouse.setCreativity(2.0);  
milhouse.setSkinnerFriend(true);

Contestant lisa = new Contestant( "Lisa" );  
lisa.setCreativity(10.0);  
lisa.setKnowlegdeScore(10.0);  
lisa.setPresentationScore(10.0);  
lisa.setSkinnerFriend(false);

scienceFair = new ScienceFair();  
scienceFair.addContestant( milhouse );  
scienceFair.addContestant( lisa );

assertEquals( milhouse, scienceFair.getWinner() );  

Testamos a lógica da nossa aplicação no ponto mais próximo de onde ela está, ou seja, fazendo chamadas às classes responsáveis por defini-las. Isso faz com que nossos executem muito rápido (< 1 ms) e caso seja encontrado um problema, a porção de código onde o defeito pode estar é pequena, facilitando o diagnóstico de erros (leia-se menos debug).

Testes de integração

Testes de integração

Uma lógica correta é apenas uma parte do problema resolvido. Para que a aplicação funcione corretamente é necessário que os dados sejam persistidos e exibidos corretamente. Como a maioria dos problemas relacionados à persistência estão ligados a queries mal formadas vamos recorrer aos testes de integração. O teste de integração é semelhante ao teste unitário, porém ele roda em conjunto com ambiente, no nosso caso o banco.

Um dos pontos que comentei é o princípio dos testes independentes, que tem como uma das premissas deixar o ambiente inalterado. Isso permite que rodemos os testes indefinidamente sem termos erros de constraints (por alguma chave no banco duplicada) e evita que um dado salvo em um teste venha a atrapalhar outro.

Uma ferramenta que nos ajuda a atingir esse objetivo no mundo Java é o AbstractTransactionalSpringContextTests, uma classe do Spring que podemos herdar na classe de teste. Essa classe faz com uma transação seja iniciada antes da execução de cada teste e seja desfeita no final, deixando o banco inalterado. Então ganhamos a limpeza de graça.

A partir desse teste vamos criar nossa classe de repositório e as configurações no spring:

ScienceFair scienceFair = new ScienceFair();  
scienceFair.addContestant(new Contestant("lisa"));  
scienceFairRepository.save( scienceFair );

assertEquals( scienceFair, scienceFairRepository.getCurrent() );  

Testes de sistema

Testes de sistema

Dado que nossa lógica de negócio e persistência estão funcionando é necessário verificar se os componentes trabalham juntos e se os dados serão exibidos corretamente.

Precisamos escolher uma tecnologia, como comentei as programáveis são melhores. No nosso caso uma tecnologia que se encaixa bem nesse perfil é o WebDriver. Ele disponibiliza uma opção de rodar os testes usando o HtmlUnit, um engine de browser sem interface, que permite que nossos testes rodem mais rápido [1].

Os testes do WebDriver podem ser programados em java, assim podemos usar as classes de nossa aplicação para ajudar nos testes. Por exemplo, se precisarmos salvar um participante da feira de ciências, podemos chamar a classe de Repositório da aplicação ao invés de escrever um SQL. Isso faz nossos testes menos frágeis à refactorings, mais rápidos de serem escritos e mais fáceis de serem mantidos.

Vamos começar com a funcionalidade de cadastrar participante. Observe que o Sr. Skinner não pediu uma funcionalide de visualizar participantes, então como faremos para testar do ponto de vista da aplicação? Nesse caso vem bem a calhar o princípio de foco em uma funcionalidade específica, ou seja, o teste de uma funcionalidade deve ser independente de outras funcionalidades.

No nosso caso cadastramos um participante e verificamos se ele foi salvo corretamente no banco. Os testes abaixo também fornecem um exemplo interessante de alguns padrões de testes unitários Object Mother, Guard Assertion e Custom Assertion.

public void testAdd_contestant_saves_contestant() throws Exception {  
    Contestant bart = TestObjects.getBart(); // Object mother http://martinfowler.com/bliki/ObjectMother.html

    goTo(AddContestantPage.class);

    type("name",bart.getName());
    type("knowledgeScore",bart.getKnowledgeScore());
    type("creativityScore",bart.getCreativityScore());
    type("presentationScore",bart.getPresentationScore());
    check("skinnerFriend", bart.isSkinnerFriend());

    click( "save" );

    ScienceFair scienceFair = scienceFairRepository.getCurrent();

    List<Contestant> contestants = scienceFair.getContestants();
    // Guard assertion http://xunitpatterns.com/Guard%20Assertion.html.
    assertEquals(1, contestants.size());
    // Custom assertion http://xunitpatterns.com/Custom%20Assertion.html
    assertContestantEquals(bart, contestants.get(0)); 
    }

Agora vamos testar se a aplição exibe o campeão corretamente:

public void test_winner_has_the_highest_score() throws Exception {  
    // Object mother http://martinfowler.com/bliki/ObjectMother.html
    Contestant lisa = TestObjects.getLisa();  
    Contestant bart = TestObjects.getBart();

    ScienceFair scienceFair = scienceFairRepository.getCurrent();
    scienceFair.addContestant( lisa );
    scienceFair.addContestant( bart );        
    scienceFairRepository.save(scienceFair);

    endTransaction(); // Para comitar a transação, senão a aplicação não vai achar os participantes.
    startNewTransaction();
    setComplete();

    goTo(ShowWinnerPage.class);

    assertHasText( lisa.getName() );
    assertHasText( formatDouble( lisa.getScore() ) );
}

E caso não tenha nenhum participante cadastrado, informa ao usuário:

public void test_if_doesnt_has_contestant_show_message() throws Exception {  
goTo(ShowWinnerPage.class);  
assertHasText( "Nenhum participante cadastrado!" );  
}

Observe que no caso de exibir o campeão corretamente testamos apenas o caso básico. Isso é suficiente, já que os outros casos foram testados em níveis mais baixos. Essa estratégia nos ajuda a ter uma base de testes saudável.

O projeto completo pode ser baixado aqui, junto com ele existe um script ant para subir o hsqldb (banco). Apesar de eu não ter mostrado passo a passo, esse projeto dá uma idéia de como fazer um sistema com TDD.

Caso tenha alguma dúvida ou crítica, fique a vontade para comentar.


[1] Apesar do HtmlUnit ser bastante valioso nos builds, no desenvolvimento dos testes é melhor usar o Firefox para não diminuir o contato da equipe com a aplicação