Tests

Fornece a base para testes integrados e unitários a ser utilizada, de uma forma geral, complementando seus testes escritos JUnit e JMock.

Testes

Como desenvolvedores estamos acostumados a criar. Geralmente achamos que nossos códigos, por seguirem um racional bem definido, estão completamente controlados e previsíveis. O problema de se pensar assim é que o código que criamos é uma entidade viva, e consequentemente feito para mudar, pode não ser hoje, pode não ser amanhã, mas com certeza ocorrerá e quando isto ocorrer precisamos ter certeza de que ele está manutenível. Achar que seu código está ‘bom’ não garante que ele esteja, é necessário provar e para isto nada melhor do que criar testes automatizados.

Este tipo de teste oferece diversos benefícios (se feitos corretamente):

  • Garantem a manutenibilidade: manutenções e refactors serão realizados com segurança, pois os testes vão garantir que qualquer coisa que fique instável seja detectada prematuramente. Isto deveria matar o medo clássico que alguns legados causam nos desenvolvedores de códigos intocáveis, aquele código que ninguém quer mexer porque não sabe o que pode quebrar.
  • Documentação viva: testes devem ser claros, não adianta nada criar testes obscuros e que o desenvolvedor vai perder mais tempo tentando entendê-lo do que alterá-lo. Se seus testes forem claros, eles servem como ponto de entrada para outros desenvolvedores, já que eles estarão definindo o que seu código pode ou não fazer e qual o comportamento imaginado pelo criador original.
  • Melhoram seu código: independente do uso de metodologias como TDD ou BDD, seu código só será beneficiado pela adoção de testes. Além de ficar mais seguro (afinal, você está garantindo que ele funciona), muitas vezes mostram partes de seu código que estão muito complicadas e podem precisar de ajustes.
  • Evitam regressão: se a cada problema encontrado e corrigido um novo teste for criado para garantir que aquele aspecto continue funcionando, evitamos um dos piores problemas de desgastamento entre empresas e clientes pois evitamos que futuras alterações de código coloquem novamente aquele bug no sistema.

É importante ressaltar que criar testes não garante que todo o sistema estará protegido, trata-se de um exercício contínuo em que a qualidade do código vai aumentando com a quantidade de testes criados. A ideia é garantir que o que foi feito esteja sempre protegido e que qualquer alteração na forma de funcionamento seja intencional.

Todo bom desenvolvedor se preocupa com seu código. Assim como escrever código limpo e claro já virou um hábito natural, escrever testes também pode ser. É apenas uma questão de exercício e carinho. Se quiser, ignore todas as vantagens que testes te oferecem, mas crie-os porque você é mais do que um simples “garoto que programa”, é um profissional que valoriza a empresa em que trabalha e cuida do que faz.

Estrutura

Testes nada mais são que pedaços de código que testam outro código. Do ponto de vista técnico são classes que testam outras classes. Estas classes geralmente possuem diversos métodos, e cada um deles deveria testar um ponto específico. Para padronizar a linguagem utilizada aqui:

  • Testes: código que garante o funcionamento de outro código
  • Test Case: test case possui vários testes que garantem um determinado ‘cenário’ de uso
  • Test Suits: um conjunto de Test Cases, geralmente, além do agrupamento lógico, é utilizado para ajudar a determinar a ordem que os Test Cases são executados

A imagem abaixo ilustra o que explicamos acima:

../../_images/Testes.png

Tipos de Testes

Existem alguns frameworks que se destacam na área de testes, sendo os mais conhecidos o JUnit e JMock. A estrutura oferecida pelos módulos de teste do [[Jmine]] disponibiliza classes para a criação básica de testes, de forma a ser usada complemento ao já oferecido pelos frameworks citados. O componente de testes do Jmine está disponível para uso, isso não significa a obrigação de usá-la.

A literatura técnica geralmente divide os testes em algumas categorias, não cabe a este artigo descrever todas elas, mas existem duas que se destacam e são de entendimento necessário para a boa criação de testes:

  • Unitários
  • Integrados

Explicaremos abaixo o que são estes dois tipos de testes. É importante que o desenvolvedor tenha em mente a diferença entre eles, é extremamente indesejável criar testes que carregam uma base de dados (por exemplo), apenas por costume, sem a necessidade de usá-lá.

Fluxo de um Teste

De forma geral, um teste pode ser descrito em três etapas:

  • “Dado” (Given): onde são reunidas as informações necessárias para o teste
  • “Quando” (When): os comportamentos testados são executados
  • “Então” (Then): realização de asserções de que o resultado obtido era o esperado

Em um teste bem escrito, as condições e asserções são bem claras. Além disto, existe um fluxo bem definido para a execução de cada teste. O quadro abaixo mostra o fluxo realizado pelo JUnit3:

setUp() // executado antes de cada teste
teste()
tearDown() // executado após cada teste

Os testes que usam a estrutura do Jmine-Testes possuem alguns pontos adicionais no fluxo, sendo o mais importante o initializeTestData():

initializeTestData() // executado somente uma vez por test case, antes de qualquer teste ser executado
setUp() // executado antes de cada teste
teste()
tearDown() // executado após cada teste

É importante ressaltar que somente a partir do Junit 4 o uso de anotações foi adotado, por isto se fez necessário uma estrutura comum para execução de testes. Na versão 4, as seguintes anotações são utilizadas:

@BeforeClass // Executado uma vez antes de qualquer teste ser executado
@SetUp // Executado antes de cada teste
@Test // Identifica um teste
@TearDown // Executado após cada teste
@AfterClass // Executado uma vez após todos os testes terem sido executados

Testes Unitários

Testes ‘leves’ e sem integração com código externo, têm o objetivo de testar um determinado comportamento e, por isto, quando necessário usamos Mocks para ‘simular’ dependências e controlar o ambiente. O componente unit-test do Jmine Tec fornece a base para alguns padrões encontrados quando desejamos criar testes unitários.

../../_images/Teste_unitario.png

Na imagem acima, exemplificamos um teste unitário. É possível reparar que estamos testando apenas um pedaço específico de código e ignorando (ou simulando) suas dependências. A ideia do teste unitário é exatamente essa, testar unidades pequenas de código e garantir que os detalhes estão funcionando corretamente no nível mais baixo.

Builders

Um Builder é uma estrutura que assume a parte de criação de entidades, deixando o código de testes mais limpos. Um builder só precisa implementar a interface Builder, o método build() deve retornar um objeto da entidade proposta.

Test Cases

A classe TestCase tem função análoga ao que já foi explicado (uma classe que possui diversos testes) e geralmente é utilizada em testes de JUnit3. Há também implementações específicas para alguns casos, como é o caso do DoubleTestCase, que testa valores do tipo Double, cuidando de problemas de arredondamento. O EnhancedTestCase fornece alguns hooks adicionais no fluxo de testes, como por exemplo ‘tearDownAfterClass’ e ‘setUpBeforeClass’.

A menos que você tenha motivos muito fortes para não fazer isto, aconselhamos o uso de JUnit4 e suas anotações sempre que possível.

Performance

Estrutura básica para criar testes de performance e relatórios.

Testes Integrados

Ao contrário dos testes unitários, os integrados englobam as dependências e testam sua comunicação com o componente testado. Veja a imagem abaixo, ela exemplifica este cenário:

../../_images/Teste_integrado.png

Note que um teste integrado é mais ‘caro’ de ser executado, pois há todo um contexto que precisa ser inicializado antes que as ações possam ter seus resultados avaliados. Além disto, como estamos falando em sistemas legados, é extremamente difícil garantir seu comportamento.

Em geral, é de bom tom criar testes unitários que simulem estas dependências para garantir que o código faz o que se propõe a fazer e também ter testes integrados que cuidam de outro nível de teste, testa se tudo está se comportando como previsto, sem repetir os testes já realizados unitariamente.

Testes com Banco de dados

Um dos testes integrados mais comuns são os que dependem de banco de dados, a estrutura para este tipo de teste se encontra no componente persist e oferece TestCases específicos que manipulam as bases de referência utilizadas, sendo os mais importantes:

  • DBTestCase: Prepara uma base de dados limpa, ou seja, somente com seu Schema preparado
  • RefDBTestCase: Após montar o Schema da base, pode carregar o PostScript ou Base de Referência (maiores detalhes sobre isto no componente persist)

Estes Test Case tentam otimizar o uso do banco de dados e sempre entregar a melhor base para o teste. Para isto eles criam dumps para cada Test Suit, e entre cada Test Case limpam a base, importando novamente os dumps caso algum teste marque a base como ‘suja’ (com o método markDirty() do Environment)

Além disto repare que existem duas versões para estas classes:

  • JUnit3: funciona através de herança e métodos abstratos para saber qual base carregar
  • JUnit4: funciona através de anotação, neste caso a @DBEnv

Também temos um Test Case para de Objetos de Negócio (Business Objects), o BOTestCase testa operações básicas em entidades e ainda fornece entidades para outros testes através dos métodos getUnsavedData() e getSavedData().

Outro teste que merece ser citado é o StaticAnalysisTest, que verifica todos os mapeamentos do Hibernate em busca de inconsistências.

Linguagem Específica de Domínio: Planilhas

Até agora falamos apenas sobre testes que são criados por desenvolvedores, mas há casos em que é interessante que os analistas escrevam testes de negócio. Este é o cenário ideal, mas analistas geralmente entendem de negócio e não de código, por isto é necessário criar uma linguagem específica de domínio com a qual eles possam expressar os testes de negócio da forma mais familiar possível.

A linguagem para validação de regras de negócio que criamos é baseada em planilhas. É possível realizar algumas ações (utilizando serviços, como explicado mais para frente) basicas de cadastro, execução e validação de cenários. As planilhas são lidas, interpretadas e seus resultados são validados em um cenário completo de testes.

Além de ser um recurso fácil de utilizar e extremamente familiar a qualquer analista, serve como documentação do próprio negócio.

Serviços

Toda a leitura de planilhas é realizada através de serviços (veja o componente Jmine Tec Services), para testar um cenário é necessário preparar os serviços antes. Os serviços mais comuns são:

  • Incluir
  • Executar
  • Validar

Test Runner

O Test-Runner é programa que permite executar planilhas com cenários de teste, é extremamente importante como complemento à linguagem de planilhas criada, pois é assim que os analistas executam e testam regras de negócios. Muitas vezes, quando um problema é relatado pelo cliente, o analista cria um cenário equivalente e executa no Test-Runner para verificar e comprovar o problema.