Pesquisar este blog

segunda-feira, 26 de outubro de 2015

Testes de código no Java


Dependências

As dependênicias que devem ser adicionadas ao projeto para usar as ferramentas apresentadas neste post são:

<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>

Testes no desenvolvimento de software

Todos sabem que o desenvolvimento de software muitas vezes não atende plenamente o que foi especificado pelo cliente.
Existem muitos processos de desenvolvimento de software, cada um com suas vantagens e desvantagens.
Existem vários artigos científicos explicando que o custo de conserto de um bug é exponencial em relação ao tempo que ele foi descoberto. Quanto mais cedo for descoberto um bug, mas fácil é de se arrumar ele. Isto acontece devido a memória do programador, se o bug é descoberto durante o desenvolvimento, o programador tem na cabeça o funcionamento daquele código e com isso fica bem fácil de arrumar o problema. Quanto mais tempo passa, após o desenvolvimento, mais difícil será para o programador lembrar do código, com isso ele leva algum tempo até ler e entender o funcionamento do código.
Por mais legível que seja o código, haverá uma perda de tempo para ler e entender o código.
Bugs nos softwares podem ser classificados em diversas formas:
  • atende aos requisitos especificados na documentação da análise
  • responde corretamente aos dados de entrada
  • realiza suas operações em um período aceitável
  • é utilizável
  • pode ser instalado e rodar nos ambientes planejados
  • atinge os resultados que os clientes desejam
Existem várias formas de se encontrar um bug no software e também existem várias intensidades de um bug, por exemplo uma cor errada em um botão é certamente menos grave que um bug que calcula errado o saldo da sua conta no banco.
A intensidade do bug varia de negócio para negócio, mas certamente um dos objetivos de quem desenvolve software é diminuir ao máximo a quantidade de bugs nos seus sistemas.
E como fazer isso? Existem uma série de ferramentas que nos ajudam a validar a qualidade do código, e as farramentas de teste unitários são uma delas.

Teste de unidade

O teste de unidade testa as assinaturas, as entradas e saídas de uma unidade de software. Por unidade de software entenda como a menor parte testável de um software. Por exemplo em um programa orientado a objeto, um método pode ser uma unidade a ser testada.
As entradas e saídas, podem ser os parâmetros do seu método e as respostas que o seu método retorna após ser executado. 

Ferramentas de Teste Unitário para o Java

JUnit

Uma das ferramentas mais conhecidas para a execução de testes unitários é o JUnit. Esta ferramenta fornece métodos e anotações que facilitam a execução de códigos de testes.
Por exemplo 
O processo de desenvolvimento de software exerce um papel bem importante na quantidade de bugs. Existe uma técnica conhecida como Desenvolvimento Guiado por Testes, ou Test Driven Development (TDD) que prega que devemos criar primeiro um teste para depois escrevermos o código.
Por exemplo, imagina que você esteja desenvolvendo uma calculadora e o seu código está assim:
public class Calculadora {
  public int operacao(String expressao) {
    int resultado = 0;
    for (String soma: expressao.split("\\+"))
      resultado += Integer.valueOf(suma);
    return soma;
  }
}
Por enquanto esta sua calculadora só faz a soma, e claro como ela é muito simples não estamos aplicando técnicas de orientação a objeto nela, então um teste válido para ela seria:
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class CalculadoraTest {
  @Test
  public void avaliaAExpressao() {
    Calculadora calculadora = new Calculadora();
    int soma = calculadora.operacao("1+2+3");
    assertEquals(6, soma);
  }
}
A classe de teste é uma classe java normal, a única diferença aqui é que a anotação @Teste marca quais são os métodos que vão testar o código, o que neste caso estamos avaliando uma operação de soma. O método assertEquals valida se o resultado retornado pelo método soma é igual a 6, assertEquals valida se o primeiro atributo é igual ao segundo.
Mas não temos o == para validar a igualdade, por que então é que usamos o assertEquals?
O JUnit provê vários métodos que inciam como assert e nos ajudam a validar dados, e usar eles facilita a nossa vida, pois quando uma validação falha, ele automaticamente gera um relatório daquela falha.

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class CalculadoraTest {
  @Test
  public void avaliaAExpressao() {
    Calculadora calculadora = new Calculadora();
    int soma = calculadora.operacao("1+2+3");
    assertEquals(6, soma,"Resultado da soma inválido");
  }
}
Os métodos de assert possuem um argumento de mensagem, que será colocada no log dos testes e pode nos ajudar a achar o problema no caso de uma falha.

Mock de objetos

O JUnit é muito bom para testar as regras do código que não dependem de nenhum serviço externo, como uma classe de outra camada, um banco de dados, algum webservice ou mesmo uma api de terceiros.
Mas e quando o código a ser testado depende de algo externo?
Um dos intuitos dos testes unitários é testar apenas uma parte do código, eles fazem parte do processo de desenvolvimento e devem rodar bem rápido. Quando o código depende de algo externo, um banco de dados, ou de um serviço externo, pode impactar bastante  na performance de execução de um teste de unidade. 
E como fazemos para resolver este tipo de problema?
Uma das formas de resolver isso, é usando um objeto Mock, um Mock nada mais é que um simulador de resultados, com um mock podemos simular qual vai ser o resultado de um select no banco, sem precisarmos nos conectar a ele, com isso conseguimos simular o funcionamento da parte do código que realiza operações no banco de dados, sem ter que acessá-lo.
EasyMock e JMock são dois exemplos de frameworks de mock para Java.
Chamamos de mock a instância de uma classe que vamos simular, por exemplo ao testar o validador, apresentado aqui, usariamos o mock para mockar os objetos que não são usados no teste:

public class ValidadorDeSenhaTest extends EasyMockSupport{

   private MessageFactory msg = createNiceMock(MessageFactory.class);

   @TestSubject
   private final ValidadorDeSenha validador = new ValidadorDeSenha(msg);

   @Rule
   public ExpectedException excecao= ExpectedException.none();

   @Mock
   private FacesContext context;
   @Mock
   private UIComponent uiComponent;

   @Test()
   public void testSenhaErrada() {
       String mensagem = "Mensgem";
       excecao.expect(ValidatorException.class);
       excecao.expectMessage(mensagem);
       String valor = "1234A";      
       expect(msg.getMessage("senhaInvalida")).andReturn(mensagem);
       replay(msg);
       validador.validate(context, uiComponent, valor);
   }
Esta implementação foi feita através do EasyMock, e podemos notar isso logo na extensão da classe EasyMockSupport, esta classe irá ajudar a trabalhar com os mocks.
O primeiro ponto deste teste é a utilização do método createNiceMock na classe MessageFactory, este método é utilizado para criarmos um mock da classe MessageFactory, esta classe será criada sem nenhum parametro, ou método implementado, por enquanto.
Esta classe será utilizada no construtor da classe ValidadorDeSenha, que vai ser o nosso sujeito do teste (@TestSubject), esta anotação está nos dizendo onde é que os objetos mockados serão utilizados.
O método de validação dispara uma exceção quando a validação falha, por isso usamos a anotação @Rule para criar uma regra de exceção. Dentro do método de teste este objeto, excecao, será configurado para esperar uma exceção do tipo ValidatorException, com a mensagem "Mensagem". Estas configurações foram adicionadas para validar se realmente o objeto mock foi utilizado, já que a mensagem disparada pela implementação é a mensagem configurada no arquivo de properties.
Para mockar um objeto, basta adicionarmos a anotação @Mock sobre os objetos mockados, e como o método validador recebe um objeto FacesContext e um UIComponent, que não serão utilizados na sua implementação, podemos mocka-los sem problemas.
Repare que o método expect é utilizado para dizer ao mock que haverá uma chamada ao método getMessage, com o parametro "senhaInvalida", e este método quando chamado irá retornar a mensagem "Mensagem".
Quando um mock é criado ele não possui nenhuma implementação de método e se algum método do mock for executado, ele irá disparar uma mensagem. O método expect server para simular chamadas aos métodos, podendo determinar o seu retorno e isso é que torna possível emular uma camada do nosso software, como um banco de dados.
Neste exemplo há apenas uma chamada a um método, mas poderiam ser várias, o único detalhe é chamar o método replay depois de configurar todas as chamadas a métodos, com isso o easymock implementa todas as expectativas no objeto mockado e o teste pode ser executado.

Teste de integração

O teste de integração é feito em sistemas que possuem conexões com outros sistemas, este teste visa validar se a troca de informações entre diversos sistemas acontecem como o esperado.
Imagine um sistema de banco, que deve-se conectar com o sistema de outros bancos para trocar informações das transferências interbancárias realizadas.
Estas informações trocadas entre os sistemas, devem obedecer a um padrão estabelecido pelo Banco Central, porém para o banco, é muito importante que esta troca de informações seja testada, para evitar problemas na contabilização destas operações entre bancos.

Teste de regressão

Ao trocarmos a versão de um software, algumas funcionalidades podem sofrer alterações e é pra isso que servem os testes de regressão, eles irão testar se as funcionalidades de uma versão, continuam funcionando da mesma forma, em uma versão futura do software, e ele garante que uma nova versão não irá quebrar o que já funcionava bem na versão anterior.

Processo de Desenvolvimento

Não há dúvidas que os testes são uma parte muito importante no desenvolvimento de software, mas eles em si, não garantem que todos os bugs serão eliminados. 
Em um processo de desenvolvimento de software, quando um bug for encontrado, deve-se arrumá-lo e criar um teste para aquele caso, que provavelmente não estava sendo testado antes, senão o bug não existiria. Mas se o bug já foi arrumado, por que deve-se criar um teste para ele? Assim como o software foi para produção com aquele bug uma vez, nada impede que no futuro um desenvolvedor volte a alterar aquele código, de forma a recriar o bug. O teste criado durante a correção do bug, garante que ele não vá acontecer em uma versão futura, e se acontecer o teste irá falhar gerando um alerta para o desenvolvedor.

Test Driven Development (TDD)

O TDD prega que devemos implementar primeiro os testes e depois o código, com isso o desenvolvedor consegue planejar e pensar melhor no código a ser implementado naquale funcionalidade do sistema. 
No primeiro exemplo deste post, há uma calculadora, mas esta calculadora só executa a soma. Assumindo que esta calculadora irá implementar somente as operações básicas, soma, subtração, divisão e multiplicação, usando o TDD, o primeiro passo seria implementar um teste para cada uma destas operações. Com isso todos os testes falhariam no começo, já que a implementação do código da calculadora, ainda nem começou.
Passo a passo o desenvolvedor vai implementado os métodos, de maneira que os testes passem e o seu trabalho só termina quando todos os testes passarem.
Se todas as regras de negócio da calculadora, o que neste caso eram as operações aritiméticas básicas, estiverem sendo testadas, ao final deste processo ele garante que as regras foram implementadas.
Desenvolver software usando TDD, obriga o desenvolvedor a pensar nos cenários de teste antes de implmentar a regra de negócio e com isso, quando ele for implementar as regras, ele vai fazer isto pensando em evitar os erros pré planejados nos testes. Isto faz com que a qualidade do software implementado por ele aumente.
Mas o TDD não é feito somente de vantagens, e ele também não se encaixa em todos os projetos.
Em alguns casos, quando o projeto as regras são alteradas constantemente, usar TDD pode atrapalhar, já que ficar refazendo os testes toda hora terá um custo de desenvolvimento. Além das regras, pode ser que o design do projeto também se altere algumas vezes.É claro que um projeto que as regras ou o design são alterados constantemente tende a não ser um bom projeto.
Testes de algoritmos muito complexos serão difíceis de implementar, o que pode atrazar o desenvolvimento do produto.
Alguns autores citam que o tempo adicional de se escrever o código do teste é uma desvantagem deste processo, mas isto é muito discutido uma vez que a cada bug evitado pelo teste, estará evitando um grande e moroso processo de abertura de um problema no sistema, podendo impactar na experiência do usuário.

Usar ou não?

Há uma grande discussão na comunidade de desenvolvimento des software sobre usar ou não usar testes unitários. Existem vários argumentos contra e vários argumentos a favor.
Muitas pessoas argumentam que desenvolver testes é uma perda de tempo, pois "perde-se"  muito tempo desenvolvendo os testes, quando este tempo poderia ser usado para desenvolver mais funcionalidades.
É certo que ao desenvolver testes junto com o código, gasta-se mais tempo no desenvolvimento, mas entenda que este tempo gasto a mais, certamente será compensado ao evitar erros.
Não há dúvida que fazer testes unitários poupa tempo no desenvolvimento de software, entenda que o ciclo de desenvolvimento de um software é um processo longo, e garantir que as regras de negócio estão funcionando, e continuam funcionando a cada build, é um passo importante para garantir a qualidade no processo de desenvolvimento.
O tempo gasto para se corrigir um bug, assim com o seu custo, é exponencialmente maior, quanto mais tempo se passou do desenvolvimento daquela funcionalidade, assim quanto antes descobrimos um bug, mais fácil será para arrumá-lo.
Criar testes que exercitam as regras de negócio implementadas, ajuda o desenvolvedor a descobrir erros em sua implementação e como a implementação dos testes é feita junto ao desenvolvimento do software, os bugs encontrados serão corrigidos bem mais facilmente.
Mesmo que o teste implementado não ache nenhum problema, seu tempo de implementação será compensado pela garantia de que aquela regra continuará funcionando, uma vez que uma modificação futura poderia quebrar aquela regra, o teste garante que isso não vai acontecer, e caso aconteça, o desenvolvedor será avisado.

Ferramentas de análise de código

Existem várias ferramentas de análise de código, como o PMD. Estas ferramentas fazem busca por códigos que nunca são utilizados, possíveis bugs, como as NullPointerExceptions, complexidade ciclomática das classes, relativo a quantidade de caminhos possíveis dentro do código, ifs e whiles vazios, código duplicado.
FindBugs é uma outra ferramenta de análise de código que visa encontrar problemas na implementação, como acesso de índices fora de um array, objetos não utilizados, código fora de padrão.
Este tipo de análise contribui para a qualidade do código desenvolvido por um time, ajudando na padronização do código desenvolvido. Estas ferramentas também podem calcular quanto tempo será gasto para tornar arrumar código do software analisado, como acontece com o Sonar, que é uma plataforma que une diversas destas ferramentas afim de facilitar a delas no processo de desenvolvimento. O sonar cobre desde arquitetura, estilo de código, quanto bugs, possíveis bugs e comentários.

Cobertura de código

Um outro tipo de ferramenta bastante utilizado junto com os testes, são as ferramentas de cobertura de código.
Como exemplo temos o emma, o coverage, o cobertura e o JaCoCo. Estas ferramentas marcam quais partes do código foram testadas, fazendo com que fique fácil vizualizar o que não está sendo testado e assim contruir um teste para passar por aquela parte.
Mas devemos ter 100% de cobertura de código, ou seja, os nossos testes devem passar obrigatoriamente por todo o código desenvolvido? Isso garante que o software será livre de bugs?
Não, nenhum, nem outro, quanto maior for a cobertura de código por testes, mais tempo será gasto no desenvolvimento deles. Não há um número exato de cobertura que garanta que o codigo está bom, e nem devemos perseguir este número.
Estas ferramentas não garantem que todas as possibilidades do software foram testados, elas só mostram para o desenvolvedor qual parte do código ainda não foi testada. A grande vantagem destas ferramentas é apontar ao desenvolvedor quando uma parte do código, que deveria ter sido testada, ainda não foi. Assim fica fácil vizualizar e construir um teste para cobrir o que deveria ter sido testado mas não foi.

Nenhum comentário:

Postar um comentário