artigo
Alexandre Gazola
([email protected]): é bacharel em
Ciência da Computação pela Universidade
Federal de Viçosa (UFV) e mestre em
Informática pela PUC-Rio. Trabalha como
analista de Sistemas no BNDES e possui as
certificações SCJP, SCWCD e CSM. Mantém um
blog em http://alexandregazola.wordpress.
com.
Automatização de testes de persistência
com FIT, DBUnit e HSQLDB
Obtenha o máximo de qualidade, eficiência e clareza escrevendo
testes de integração automatizados com FIT, DbUnit e HSQLDB
A escrita e automatização de testes de unidade,
integração e aceitação é uma prática que tem
se mostrado bastante efetiva na construção
de sistemas de software de qualidade. Em
particular, é importante que os testes possam
ser executados frequentemente e que eles não
dependam de recursos externos que não estejam
sob o devido controle. Além disso, é igualmente
importante que os testes sejam escritos de forma
clara, facilitando a manutenção e contribuindo
para documentar o sistema. Neste artigo,
apresentamos uma abordagem eficaz para a
escrita de testes de integração para a camada de
persistência de aplicações com bastante acesso
e manipulação de dados. Serão utilizadas como
ferramentas de suporte os frameworks FIT e
DBUnit e o sistema de banco de dados HSQLDB.
66 www.mundoj.com.br
B
astante popularizada pelos métodos ágeis, a escrita e automação de testes de unidade, integração e aceitação, bem como o
Test-driven Development (TDD), têm se consolidado como práticas bastante
efetivas para a produção de sistemas de software de qualidade. Software que
possui boa cobertura de testes de unidade e integração, além de ter código
mais coeso e menos acoplado, é mais robusto e de mais fácil manutenção.
Num nível acima, podemos ter testes de aceitação, os quais, na terminologia
do Extreme Programming (XP), servem como “critério de pronto” para as histórias (user stories) a serem implementadas, ou seja, quando todos os testes
de aceitação estiverem sendo executados com sucesso, isso significa que o
sistema atende funcionalmente aos requisitos propostos.
Para serem eficazes, é muito importante que os testes sejam executados
frequentemente (geralmente num servidor de integração contínua), o que
implica que cada teste tenha um tempo de execução curto (idealmente,
alguns segundos). Sendo assim, é bastante desejável que os testes possam
ser executados de forma independente, sem depender de recursos externos
que não estejam sob controle (ex.: banco de dados, rede etc.). Além disso, o
código dos testes deve ser mantido com a mesma qualidade do código de
produção, o que inclui clareza. Quanto mais expressivos forem os testes, mais
fácil será mantê-los e melhor eles servirão como documentação do sistema.
"SUJHPt"VUPNBUJ[BÎÍPEFUFTUFTEFQFSTJTUÐODJBDPN'*5%#6OJUF)42-%#
Abordagens baseadas em Behavior Driven Development (BDD, desenvolvimento baseado em comportamento), por exemplo, têm se popularizado
bastante nos últimos anos. A ideia é fazer com que os testes sejam escritos
como descrições de cenários que atendam aos requisitos desejados.
Neste artigo, o objetivo é mostrar como escrever testes de integração eficientes e expressivos para a camada de persistência de uma aplicação. Na
parte inicial do artigo, daremos uma visão geral sobre testes de integração
e descreveremos as características básicas das ferramentas que serão utilizadas como apoio. Em seguida, exploraremos como escrever os testes de
integração para uma pequena aplicação de exemplo. Por fim, faremos um
breve comentário sobre a abordagem de testes de unidade para camadas
de persistência.
Este artigo assume que o leitor já possui conhecimentos básicos sobre testes
de unidade e seus benefícios. Para uma boa introdução, conferir o artigo
“Testes unitários para camadas de negócios no mundo real”, da edição 23 da
Mundoj.
Testes de integração
Afinal de contas, o que são testes de integração? Nada mais do que testes que
validam o resultado da interação de um conjunto de módulos de um sistema.
Testes de integração se distinguem dos testes de unidade, pois estes últimos visam validar de forma isolada apenas uma pequena parte de código do sistema
(tipicamente, a lógica de um método). Para deixar mais claro, vamos à definição
de Michael Feathers (autor do livro clássico “Working Effectively With Legacy
Code”), que diz que um teste não é um teste de unidade se ele:
- se comunica com o banco de dados;
- faz acesso à rede;
- acessa o sistema de arquivos;
- não pode ser executado ao mesmo tempo em que outros testes de unidade;
- exige que o ambiente seja configurado de forma especial para que possa
ser executado.
Todos esses exemplos caracterizam testes de integração (por isso, não é pelo
fato de o seu teste usar o JUnit que ele pode ser considerado um teste de
unidade). Neste artigo, como estamos interessados em testar a camada de persistência de uma aplicação (com testes de integração), nosso foco recae sobre
o primeiro item citado: testes que exigem comunicação com banco de dados.
Um teste de integração com banco de dados envolve interação com diversos
componentes além da própria camada de persistência, tipicamente: o framework de mapeamento objeto-relacional (Hibernate, por exemplo), o driver
JDBC e o banco de dados relacional com seus dados e esquema. Por tudo isso,
configurar, gerenciar e executar os testes nesse ambiente pode ser bastante
custoso tanto em termos de programação (custo de criar e manter os testes)
quanto em performance dos testes (eles podem demorar bastante para executar, por exemplo, se muitas conexões forem desnecessariamente abertas).
Para endereçar esses problemas, utilizaremos em nossos testes três ferramentas de apoio, descritas resumidamente na seção seguinte. Mais adiante no
artigo veremos como essas ferramentas podem ser utilizadas e combinadas
para possibilitar a confecção de testes de integração de elevada qualidade.
Ferramentas
HSQLDB
O HSQLDB (HyperSQL DataBase) é um sistema de gerenciamento de banco
de dados (SGBD) open-source, completamente escrito em Java. A vantagem
de sua utilização é que ele é pequeno e rápido e possui suporte a tabelas em
memória ou em disco, além de suportar os modos embutido e servidor. Ele
também suporta grande parte do padrão SQL Neste artigo, utilizaremos um
banco HSQDB em memória para diminuir o overhead de um banco de dados
tradicional (dispensando acesso a disco etc.). Na data de escrita deste artigo,
a versão atual é a 1.8 e o download pode ser realizado em http://hsqldb.org/.
Apesar de o uso do HSQLDB em testes automatizados ter ótimos benefícios,
é muito importante que os testes também sejam executados sobre o sistema
de banco de dados “real”, no servidor de integração contínua. Mesmo que
um framework ORM seja utilizado, o sistema pode ter problemas com a troca
de banco de dados, por exemplo, por utilizar queries cujo comportamento é
distinto de um banco para outro (devido à variação de dialeto SQL etc).
FIT
O Framework for Integrated Tests (FIT) foi desenvolvido por Ward Cunningham com o objetivo de promover a colaboração dos usuários na escrita
de testes de aceitação baseados em tabelas HTML (as tabelas HTML do
FIT também são conhecidas como fixture tables). O framework trabalha
lendo tabelas HTML, interpretando-as e traduzindo-as em chamadas de
métodos em classes de fixtures, que são implementadas por classes Java
(assumindo Java como linguagem de programação utilizada). Pode-se
dizer que as classes de fixtures fazem a “cola” entre os testes especificados
pelas fixture tables com o código de produção sendo testado (também
conhecido pelo acrônimo SUT – System Under Test).
O FIT também costuma gerar como relatórios de saída os documentos de
testes utilizados com as células das tabelas coloridas em verde ou vermelho, de acordo com o status da execução do teste (ok ou erro), além dos
valores esperados em caso de erro. O FIT conta com três fixtures básicas:
Column Fixture, Row Fixture e Action Fixture, cada qual permitindo certo
tipo de layout e funcionalidade para as tabelas. É interessante também
fazer uso da FITLibrary, que acrescenta outros tipos de fixture, entre eles,
o SetUpFixture e a DoFixture, utilizados neste artigo. O FIT nos permitirá
escrever testes com alto nível de legibilidade e manutenibilidade. Na
aplicação de exemplo, apresentada mais adiante no artigo, explicaremos
de maneira prática o funcionamento básico do FIT. Está fora do escopo
deste artigo entrar nos detalhes do FIT. Para isso, ver o artigo “Requisitos
executáveis com FIT”, do Rodrigo Yoshima, da edição 31 da Mundoj.
É necessário colocar a lib da FITLibrary no classpath do projeto. O download da lib pode ser feito em http://sourceforge.net/projects/fitlibrary/.
Neste artigo, para rodar os testes, utilizamos o plugin do Eclipse FITRunner, que pode ser baixado em http://sourceforge.net/projects/fitrunner/.
Tendo instalado o plugin, basta criar uma nova configuração de Run no
Eclipse, indicando a pasta ou o documento HTML de entrada e a pasta
de saída (onde serão gerados os HTMLs e os arquivos de relatório sobre
a execução dos testes).
DbUnit
O DbUnit é um framework open-source que permite o gerenciamento
67
"SUJHPt"VUPNBUJ[BÎÍPEFUFTUFTEFQFSTJTUÐODJBDPN'*5%#6OJUF)42-%#
do estado de um banco de dados durante a execução de um conjunto de
testes. Ele permite que um banco de dados possa ser populado com um
conjunto de dados antes da execução de um teste e permite que o banco
de dados seja restaurado a seu estado original ao término da execução
do teste em questão. O DbUnit pode ser baixado em http://www.dbunit.
org/.
O DbUnit utiliza o conceito de dataset como abstração dos dados utilizados para testes. Geralmente, os datasets são representados sob a forma
de arquivos XML, mas existem datasets de planilhas Excel, de resultados
de consultas SQL ou até datasets criados programaticamente. Adiante no
artigo, veremos como utilizar datasets programaticamente para gerenciar o estado do nosso banco de dados entre as execuções dos testes de
integração. Para mais detalhes sobre o DbUnit, consultar o artigo “Testes
de unidade para camadas de persistência no mundo real”, da edição 24
da Mundoj.
Testes de integração para camadas de
persistência
Nesta seção, mostraremos uma abordagem para a criação de testes de
integração para a camada de persistência de uma aplicação (o exemplo
é bastante simples, apenas para facilitar a explanação da abordagem).
)JTUØSJBocomo cliente da BookPlus desejo utilizar o sistema para visualizar todos os livros que se enquadrem em uma determinada categoria
para que eu possa restringir a busca aos livros de meu interesse.
)JTUØSJBo como cliente da BookPlus desejo utilizar o sistema para visualizar todos os livros da livraria por ordem de qualificação (a qualificação de
cada livro é dada pela média das notas recebidas).
Além disso, para termos um “conceito de pronto” bem definido e também
deixarmos claro o significado de cada história, rascunhamos juntamente
com o seu Rolando os seguintes critérios de aceitação para cada uma das
histórias (bastante simples e até forçados, apenas a título de ilustração).
)JTUØSJB
1. Temos cadastrados no banco de dados do sistema os seguintes livros: A (informática), B (cristianismo), C (auto-ajuda), D (informática)
e E (matemática). Quando o cliente solicitar a exibição dos livros cuja
categoria seja informática, o sistema deverá exibir os livros A e D.
2. Temos cadastrados no banco de dados do sistema os seguintes livros: A (informática), B (cristianismo), C (auto-ajuda), D (informática)
e E (matemática). Quando o cliente solicitar a exibição dos livros cuja
categoria seja ciências, o sistema deverá exibir a mensagem “No momento, não possuímos livros na categoria ciências”.
Aplicação de exemplo
Usaremos como exemplo de motivação um sistema simples de livraria
virtual, descrito no Quadro 1. Com os requisitos expressos no Quadro 1,
temos o banco de dados com a tabela para armazenamento de livros e
também a tabela para armazenamento do valor da cotação das moedas
(em reais), conforme mostra o diagrama ER da figura 1.
-JWSBSJB#PPL1MVT
A virtual BookPlus é uma livraria como qualquer outra livraria virtual, ou
seja, vende livros na internet. Cada livro possui título, ISBN, autor, preço e
categoria. O dono da BookPlus, o seu Rolando Caio da Rocha, é um verdadeiro visionário e, por isso, deseja que o sistema permita que os clientes
avaliem os livros com uma nota de 0 a 10 para que outros clientes possam
consultar os livros mais bem qualificados.
2VBESP%FTDSJÎÍPEBBQMJDBÎÍPEFFYFNQMP
)JTUØSJB
1. Assumindo que o livro A possua sete notas 6 e uma nota 8 (qualificação = 6,25), o livro B possua quatro notas 9 e duas notas 5 (qualificação = 7,66), o livro C possua 15 notas 3 (qualificação = 3) e o livro
D possua quatro notas 4 (qualificação = 4). Quando o cliente solicitar
ao sistema os livros por ordem de qualificação, o sistema deverá retornar os livros B, A, D, C (nesta ordem).
A partir dos requisitos expressos por essas duas histórias, chegamos ao modelo
de classes exibido na figura 2. Nosso objetivo é testar a camada de persistência
da aplicação que, neste caso, é composta apenas pela classe LivroDao. A Listagem 1 contém uma implementação simples para essa classe, usando JPA (obs.:
é importante que as classes DAO (Data Access Object) implementem interfaces
para promover o isolamento entre a camada de persistência e a camada de
negócios, de forma que esta última também possa ser testada isoladamente,
através de mocks para os DAOs, por exemplo). O código da classe Livro, por sua
simplicidade (possui apenas atributos e getters ), não será exibido.
Livro
id
titulo
isbn
autor
preco
Avaliação
id_livro (FK)
nota
categoria
Figura 2. Modelo de classes da aplicação.
'JHVSB"MHVNBTUBCFMBTEBMJWSBSJB#PPL1MVT
Após uma reunião de planejamento com o seu Rolando, nosso Product
Owner na terminologia do Scrum (ou cliente sempre presente na terminologia do XP), definimos as seguintes histórias para nossa próxima
iteração:
68 www.mundoj.com.br
Testes de integração com o FIT
Começaremos escrevendo nossos testes no formato de tabelas do FIT. Qualquer editor que permita criar tabelas HTML é válido. Uma possibilidade é usar
"SUJHPt"VUPNBUJ[BÎÍPEFUFTUFTEFQFSTJTUÐODJBDPN'*5%#6OJUF)42-%#
um editor de textos para editar as tabelas e salvar o documento como arquivo
HTML. O formato de escrita do documento (com exceção das tabelas) é livre. A
figura 3 exibe nosso documento de testes no formato esperado pela classe de
fixture do FIT que utilizaremos.
private EntityManager entityManager;
public LivroDao(EntityManager entityManager) {
this.entityManager = entityManager;
}
Neste caso, a tabela inicial do documento nos informa que o nome da classe
de fixture é br.com.mundoj.LivrosFixture. O código desta classe pode ser visualizado na Listagem 2. A classe LivrosFixture herda de DbUnitDoFixture (classe
utilitária nossa), que por sua vez herda de DoFixture. Esta última é disponibilizada pela FITLibrary.
@SuppressWarnings(“unchecked”)
public List<Livro> getLivrosPorCategoria(String categoria) {
Query query = entityManager.createQuery(
SELECT_LIVROS_POR_CATEGORIA);
query.setParameter(1, categoria);
return query.getResultList();
}
Usualmente, um teste do FIT costuma ter apenas uma tabela (vinculada à sua
respectiva classe de fixture). Neste caso, para atingirmos nossos objetivos de
legibilidade, fazemos uso da característica de “Flow Mode” da DoFixture, a qual
permite que os testes possam ser divididos em diversas tabelas.
No construtor da classe LivrosFixture, instanciamos o objeto de testes livroDao
e o armazenamos num campo da classe. Continuando o processamento do
documento HTML, o FIT encontra a tabela cujo header é livros. Com isso, o
FIT sabe que deve chamar o método livros() da classe LivrosFixture (o nome
deve ser exatamente igual!). Neste método, retornamos um objeto da classe
LivrosDbUnitSetup, que é um tipo de SetUpFixture (esta fixture também faz
parte da FITLibrary).
Pelo fato de o objeto retornado ser uma SetUpFixture, o FIT irá, para cada linha
da tabela de livros, chamar o método idTituloIsbnAutorPrecoCategoria() (o
nome do método procurado é igual aos nomes das colunas da tabela). Este
método simplesmente usa um método de uma de nossas classes utilitárias
que adiciona os dados na tabela Livro do dataset programático do DbUnit (o
esquema da tabela do dataset é definido no construtor de LivrosDbUnitSetup).
@SuppressWarnings(“unchecked”)
public List<Livro> getLivrosPorOrdemDeQualificacao() {
Query query = entityManager.createNativeQuery(
SELECT_MELHORES_LIVROS,
Livro.class);
return query.getResultList();
}
}
Listagem 2. Classe de fixture para testar a persistência de livros.
public class LivrosFixture extends DbUnitDoFixture {
private LivroDao livroDao; // objeto a ser testado
public void brDotComDotMundojDotLivrosFixture() {
livroDao = new LivroDao(entityManager);
}
O código da classe LivrosDbUnitSetup pode ser visualizado na Listagem 3. No
construtor dessa classe, especificamos o esquema da tabela à qual ela está
associada (tabela de livros) e também registramos um parse delegate através
da chamada ao método registerParseDelegate. O delegate é importante para
que o FIT saiba tratar a coluna preço, cujo tipo no Java é BigDecimal (conforme
consta na assinatura do método idTituloIsbnAutorPrecoCategoria() ).
/*
* Setup do banco de dados
*/
public Fixture livros() {
DbUnitSetupFixture fixture = new LivrosDbUnitSetup();
adicionarSetupFixture((DbUnitSetupFixture) fixture);
return fixture;
}
Ao ler a célula da tabela e ver o parâmetro correspondente no método idTituloIsbnAutorPrecoCategoria(), o FIT procurará um delegate para converter
a string lida do HTML no tipo BigDecimal esperado. Esse artifício é útil para
podermos trabalhar com objetos arbitrários.
public Fixture avaliacoes() {
DbUnitSetupFixture fixture = new AvaliacoesDbUnitSetup();
adicionarSetupFixture((DbUnitSetupFixture) fixture);
return fixture;
}
Depois disso, o FIT continuará o processamento do arquivo e fará um processamento análogo ao explicado para LivrosDbUnitSetup para inserir os dados
de notas na tabela Avaliacao do dataset programático do DbUnit através da
classe AvaliacoesDbUnitSetup (esta classe não será exibida por ser bastante
semelhante à classe LivrosDbUnitSetup).
/*
* Implementação dos cenários de teste
*/
public List<Livro> livrosDaCategoria(String categoria) {
return livroDao.getLivrosPorCategoria(categoria);
}
Listagem 1. Implementação da classe LivroDao.
public class LivroDao implements RepositorioLivros {
private static final String SELECT_LIVROS_POR_CATEGORIA =
“SELECT OBJECT(L) “ + “FROM Livro AS L where L.categoria = ?1”;
public boolean naoExistemLivrosNaCategoria(String categoria) {
return livroDao.getLivrosPorCategoria(categoria).isEmpty();
}
private static final String SELECT_MELHORES_LIVROS =
“SELECT Livro.* FROM “
+ “Livro as L inner join Avaliacao as A on L.id = A.id”
+ “ order by avg(A.nota)”;
public List<Livro> livrosPorOrdemDeQualificacao() {
return livroDao.getLivrosPorOrdemDeQualificacao();
}
}
69
"SUJHPt"VUPNBUJ[BÎÍPEFUFTUFTEFQFSTJTUÐODJBDPN'*5%#6OJUF)42-%#
Listagem 3. Classe que faz a inserção dos dados de livros no dataset do
DbUnit.
public class LivrosDbUnitSetup extends DbUnitSetupFixture {
private static final Logger logger = Logger
.getLogger(LivrosDbUnitSetup.class);
public LivrosDbUnitSetup() {
super(new DefaultTableMetaData(“LIVRO”, new Column[] {
new Column(“ID”, DataType.DECIMAL),
new Column(“TITULO”, DataType.CHAR),
new Column(“ISBN”, DataType.CHAR),
new Column(“AUTOR”, DataType.CHAR),
new Column(“PRECO”, DataType.DECIMAL),
new Column(“CATEGORIA”, DataType.CHAR), }));
registerParseDelegate(BigDecimal.class, new BigDecimalDelegate());
}
/**
* Insere um registro na tabela LIVRO com os valores dos argumentos passados
* como parâmetro.
*
*/
public void idTituloIsbnAutorPrecoCategoria(long id, String titulo,
String isbn, String autor, BigDecimal preco, String categoria) {
if (logger.isDebugEnabled()) {
logger.debug(“Inserindo livro “ + titulo + “ no dataset de “
+ “testes...”);
}
addFixtureDataToTable(id, titulo, isbn, autor, preco, categoria);
}
public static class BigDecimalDelegate {
public BigDecimal parse(String preco) {
return new BigDecimal(preco.replace(“,”, “.”));
}
}
}
Ao chegar na tabela que possui o texto “Cenarios de Testes”, o FIT chama o método cenariosDeTestes() da classe LivrosFixture, que é herdado da nossa classe
utilitária DbUnitDoFixture (mais detalhes sobre essa classe mais à frente no
artigo). Esse método é um gancho para que as tabelas definidas e populadas
anteriormente sejam de fato inseridas no dataset programático do DbUnit e
este seja inserido (via operação DatabaseOperation.INSERT do DbUnit) no
banco de dados de testes (HSQLDB em memória). Neste ponto, já temos a
70 www.mundoj.com.br
'JHVSB%PDVNFOUP)5.-DPNBFTQFDJmDBÎÍPEPTUFTUFTEP%"0OPGPSNBUPEP'*5
nossa base de testes em memória com os dados provenientes do documento
de testes.
Em seguida, o FIT continua o processamento do documento e encontra a primeira tabela do cenário 1. Mais uma vez, o header da tabela será utilizado para
acionar o método correspondente da LivrosFixture. Nesse caso, utilizamos uma
característica do FIT que nos permite passar parâmetros para o método. A regra
é a seguinte: os nomes das colunas pares são utilizados para definir o nome do
método a ser chamado e os nomes das colunas ímpares são os argumentos
passados como parâmetro para esse método. Sendo assim, o FIT irá executar o
método livrosDaCategoria(arg) da classe LivrosFixture. Nesse método, fazemos
a chamada ao método do DAO que queremos testar (i.e. getLivrosPorCategoria
()), retornando a lista com os livros da categoria especificada. Pelo fato de o
retorno do método livrosDaCategoria(arg) ser uma lista, o FIT irá comparar
cada objeto da lista com cada linha da tabela (para isso, ele faz um ‘getNomeDaColuna em cada objeto e compara com o que está na tabela de testes). Não
é necessário nenhum assert!
Continuando o processamento, o FIT encontrará a tabela seguinte, que fará
com que seja acionado o método naoExistemLivrosNaCategoria(arg). Nesse
caso, chamamos nosso método sendo testado (getLivrosPorCategoria())
e esperamos que a lista seja vazia. Pelo fato de o retorno do método ser um
booleano, o FIT sabe que deve apenas verificar se o resultado foi “true” (teste
OK) ou “false” (teste falhou).
Por fim, chegamos à última tabela de testes do FIT, já no cenário 2, cujo processamento irá incorrer na chamada ao método livrosPorOrdemDeQualificacao()
da classe LivrosFixture, cujo funcionamento é similar ao já explicado para os
"SUJHPt"VUPNBUJ[BÎÍPEFUFTUFTEFQFSTJTUÐODJBDPN'*5%#6OJUF)42-%#
testes de obtenção de livros por categoria.
<property name=”hibernate.show_sql” value=”true” />
Executando os testes do FIT pelo plugin do Eclipse usando nosso documento
HTML, veremos o FIT imprimir no console as estatísticas dos nossos testes
(cada comparação que ele faz ele considera como um teste):
<property name=”hibernate.format_sql” value=”true” />
<property name=”hibernate.hbm2ddl.auto” value=”create” />
49 tests passed
0 wrong results
0 exceptions happened
0 tests ignored
</properties>
</persistence-unit>
</persistence>
Com isso, o leitor já tem uma visão geral de como utilizar o FIT em conjunto
com o DbUnit e o HSQLDB para fazer testes de integração em camadas de
persistência. Pode-se notar que os testes descritos nas tabelas FIT da figura
3 são praticamente uma tradução direta dos critérios de aceitação definidos
anteriormente com o cliente. Isso traz o benefício de permitir que o cliente
participe de forma mais próxima, colaborando ativamente na elaboração de
cenários de testes que validem a aplicação desenvolvida. Nas seções seguintes, mostraremos a configuração feita para utilizar o HSQLDB em memória e
também entraremos em mais detalhes nas classes utilitárias da aplicação que
fazem de fato a integração entre o FIT e o DbUnit.
Configuração do banco de dados de testes
Para a execução eficiente dos nossos testes de integração, utilizamos como
banco de dados o HSQLDB acessado via JPA/Hibernate. O arquivo de configuração da JPA (persistence.xml) pode ser visualizado na Listagem 4, sendo que
as linhas em negrito se encarregam de fazer a configuração específica para que
seja utilizado o HSQLDB em memória. A última linha em negrito também é
interessante para as classes testes, pois forçam que o Hibernate crie as tabelas
(apenas as que possuem classes mapeadas com anotações da JPA) automaticamente para nós.
Conforme pode ser notado pelo código da Listagem 1, para acessar o banco
de dados o LivroDao precisa receber um EntityManager em seu construtor
(injeção de dependências, boa prática!). Sendo assim, de onde obtemos um
EntityManager? Costuma ser uma prática adotada no mercado (geralmente
em aplicações que não são gerenciadas por um servidor de aplicação) criar-se
uma classe utilitária para isso. Portanto, criamos a classe JPAUtil, cujo código
pode ser visto na Listagem 5. Nesse caso, mantivemos apenas o código usado
pelos testes. Repare que, além de ser um ponto de acesso aos EntityManager
para o banco de testes, essa classe também faz a criação da tabela de Avaliacoes. Isso acaba sendo necessário porque não temos uma classe Avaliacao
mapeada via JPA, então o Hibernate não consegue construir a tabela sozinho.
Listagem 5. Classe JPAUtil.
public class JPAUtil {
private static final String TEST_URL_CONNECTION =“jdbc:hsqldb:mem:testdb”;
private static final String TEST_JDBC_DRIVER = “org.hsqldb.jdbcDriver”;
private static final String CRIAR_AVALIACAO = “create table Avaliacao(“
+ “id_livro decimal, nota double)”;
Listagem 4. Persistence.xml.
<?xml version=”1.0”?>
<persistence xmlns=”http://java.sun.com/xml/ns/persistence”
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=”http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd”
version=”1.0”>
<persistence-unit name=”TEST”>
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name=”hibernate.archive.autodetection” value=”class, hbm” />
<property name=”hibernate.connection.driver_class”
value=”org.hsqldb.jdbcDriver” />
<property name=”hibernate.connection.url”
value=”jdbc:hsqldb:mem:testdb” />
<property name=”hibernate.connection.username” value=”sa” />
<property name=”hibernate.c3p0.min_size” value=”5” />
<property name=”hibernate.c3p0.max_size” value=”20” />
<property name=”hibernate.c3p0.timeout” value=”300” />
<property name=”hibernate.c3p0.max_statements” value=”50” />
<property name=”hibernate.c3p0.idle_test_period” value=”3000” />
<property name=”hibernate.dialect”
value=”org.hibernate.dialect.HSQLDialect” />
private Connection connection;
public EntityManager getEntityManagerForTests() {
try {
Class.forName(TEST_JDBC_DRIVER);
connection = DriverManager.getConnection(
TEST_URL_CONNECTION, “sa”,
“”);
criarTabelaDeAvaliacoesParaTestes(connection);
return Persistence.createEntityManagerFactory(“TEST”)
.createEntityManager();
} catch (Exception e) {
throw new PersistenceException(“Não foi possível criar a tabela “
+ “necessárias para testes...”, e);
}
}
public Connection getConnection() {
return connection;
}
private void criarTabelaDeAvaliacoesParaTestes(Connection connection)
throws SQLException {
connection.createStatement().execute(CRIAR_AVALIACAO);
}
}
71
"SUJHPt"VUPNBUJ[BÎÍPEFUFTUFTEFQFSTJTUÐODJBDPN'*5%#6OJUF)42-%#
Classes utilitárias para integração FIT/DbUnit
Para finalizar nosso exemplo, ainda nos falta dar uma olhada nas classes utilitárias usadas para fazer a integração entre o FIT e o DbUnit, a saber, as classes
DbUnitSetupFixture e DbUnitDoFixture. O código das duas classes está exibido
nas Listagens 6 e 7, respectivamente.
Listagem 7. DbUnitDoFixture.java.
public class DbUnitDoFixture extends DoFixture {
protected EntityManager entityManager;
A classe DbUnitSetupFixture herda de SetUpFixture (fixture da FITLibrary) e
deve ser estendida por fixtures cujo objetivo seja fazer inserção de dados no
banco de dados de testes. Conforme vimos no exemplo da livraria, ambas
as classes LivrosDbUnitSetup e AvaliacoesDbUnitSetup estendem a classe
DbUnitSetupFixture para inserir livros e notas no banco, respectivamente. A
implementação da classe DbUnitSetupFixture é bem simples. Ela recebe em
seu construtor os metadados contendo o esquema da tabela do banco (DefaultTableMetaData faz parte do DbUnit) e usa esse esquema para instanciar
uma DefaultTable do DbUnit.
protected List<DbUnitSetupFixture> setupFixtures;
private JPAUtil jpaUtil;
public DbUnitDoFixture() {
jpaUtil = new JPAUtil();
As subclasses de DbUnitSetupFixture em seus construtores especificam o
esquema de suas respectivas tabelas e passam esse esquema via chamada ao
construtor da superclasse. Além disso, ao receber os dados provenientes das
tabelas do FIT, as subclasses fazem uma chamada ao método adicionarDadosDaFixtureATabela() de DbUnitSetupFixture. Este método adiciona a lista de
objetos recebidos como uma linha na tabela do DbUnit.
entityManager = jpaUtil.getEntityManagerForTests();
setupFixtures = new ArrayList<DbUnitSetupFixture>();
}
protected void adicionarSetupFixture(DbUnitSetupFixture fixture) {
O outro método de DbUnitSetupFixture é o adicionarTabelaAoDataSet(). Esse
método é chamado pela DbUnitDoFixture para inserir uma tabela no dataset
programático do DbUnit. Como pode ser visto na Listagem 6, quando o FIT
chama o método cenariosDeTestes() de DbUnitDoFixture, terá início a inserção
dos dados que foram acumulados nas tabelas das fixtures de setup. Para isso,
instanciamos o dataset (new DefaultDataSet()) e varremos a lista de fixtures de
setup chamando o método adicionarTabelaAoDataSet() em cada uma delas.
Depois disso, temos nosso dataset pronto para ser inserido no banco de dados
usando a operação INSERT do DbUnit:
setupFixtures.add(fixture);
}
public void cenariosDeTestes() {
inserirDataSetNoBancoDeDados();
}
DatabaseOperation.INSERT.execute(new DatabaseConnection(jpaUtil.
getConnection()), dataSet);
protected void inserirDataSetNoBancoDeDados() {
DefaultDataSet dataSet = new DefaultDataSet();
Listagem 6. DbUnitSetupFixture.java.
for (DbUnitSetupFixture fixture : setupFixtures) {
public abstract class DbUnitSetupFixture extends SetUpFixture {
fixture. adicionarTabelaAoDataSet(dataSet);
private final DefaultTable table;
}
public DbUnitSetupFixture(DefaultTableMetaData defaultTableMetaData) {
table = new DefaultTable(defaultTableMetaData);
}
try {
protected void adicionarDadosDaFixtureATabela(Object... objs) {
try {
table.addRow(objs);
} catch (DataSetException e) {
throw new IllegalArgumentException(e);
}
}
protected void adicionarTabelaAoDataSet(DefaultDataSet dataSet) {
try {
dataSet.addTable(table);
} catch (AmbiguousTableNameException e) {
throw new IllegalStateException(“Invalid fixture data: “ + table, e);
}
}
}
DatabaseOperation.INSERT.execute(new DatabaseConnection(jpaUtil
.getConnection()), dataSet);
} catch (Exception e) {
throw new IllegalStateException(“Os dados de setup para os testes “
+ “de integracao não puderam ser inseridos no banco de “
+ “dados.”, e);
}
}
}
Testes de unidade para camadas de
persistência
O foco neste artigo foi a escrita de testes de integração para camadas de
persistência, isto é, testes que fazem uso de um banco de dados. No entanto,
é perfeitamente possível escrevermos “verdadeiros testes de unidade” para
nossos DAOs. Para isso, basta configurar mocks para as interfaces EntityManager e Query da JPA com as devidas expectativas e fazer os testes. A Listagem 8
traz um exemplo dessa abordagem, usando os frameworks JUnit e EasyMock.
Como o leitor pode perceber, o teste com mocks neste caso é bastante simples
e limitado, pois acaba testando apenas se os parâmetros foram passados
adequadamente na montagem da query. Para testar os aspectos mais importantes, como sintaxe e semântica das queries, é importante que empreguemos
uma abordagem baseada em testes de integração, efetivamente passando por
um banco de dados.
Testes de unidade para objetos que acessam banco de dados podem ser
uma opção (complementar aos testes de integração) para reduzir o ciclo de
feedback numa abordagem baseada em TDD, por exemplo. Eles também têm
a vantagem de permitir simular com mais facilidade certas circunstâncias e
situações por meio de mocks (por exemplo, simular a falha temporária de uma
conexão).
Listagem 8. Exemplo de teste de unidade para a classe LivroDao.
public class LivroDaoTest {
private LivroDao dao;
Saber mais
"SUJHPt"VUPNBUJ[BÎÍPEFUFTUFTEFQFSTJTUÐODJBDPN'*5%#6OJUF)42-%#
“Testes unitários para camadas de negócios no mundo real”, edição 23 da Mundoj.
“Testes de unidade para camadas de persistência no
mundo real”, edição 24 da Mundoj.
“Requisitos executáveis com FIT”, edição 31 da
Mundoj.
Considerações finais
i1SPHSBNBEPSFTQSPmTTJPOBJTFTDSFWFNUFTUFTw (ver referências). Escrever
testes automatizados para uma aplicação é uma tarefa desafiadora, que exige
conhecimento e domínio de diversas técnicas e ferramentas. Neste artigo,
mostramos uma abordagem para escrever testes de integração eficientes e expressivos para a camada de persistência de uma aplicação, utilizando HSQLDB,
FIT e DbUnit. Neste caso, os testes de integração criados foram consequência
direta dos critérios de aceitação definidos com o cliente, o que também pode
caracterizá-los como uma implementação de testes de aceitação (neste caso,
as camadas superiores da aplicação foram desconsideradas no teste, devendo
ser endereçadas com testes de aceitação end-to-end [de ponta a ponta] complementares).
Por fim, mostramos ainda como tratar o problema dos testes de persistência
usando também testes de unidade e as vantagens e desvantagens dessa
abordagem. Como sempre, é importante que o bom profissional conheça as
possibilidades e saiba aproveitar o melhor de cada uma dependendo de seu
problema específico.
“Vês a um homem perito na sua obra? Perante reis será posto, não entre a
plebe.” (Pv 22:29)
private EntityManager mockEntityManager;
private Query mockQuery;
@Before
public void oneTimeSetUp() throws Exception {
mockEntityManager = createStrictMock(EntityManager.class);
mockQuery = createStrictMock(Query.class);
dao = new LivroDao(mockEntityManager);
}
@Test
public void testGetLivrosPorCategoria() {
List<Livro> livrosDeInformatica = construirListaDeLivrosDeInformatica();
expect(mockEntityManager.createQuery(“SELECT OBJECT(L) FROM Livro “
+ “AS L where L.categoria = ?1”)).andReturn(mockQuery);
expect(mockQuery.setParameter(1, “Informática”)).andReturn(mockQuery);
expect(mockQuery.getResultList()).andReturn(livrosDeInformatica);
replay(mockEntityManager, mockQuery);
assertEquals(livrosDeInformatica, dao
.getLivrosPorCategoria(“Informática”));
verify(mockEntityManager, mockQuery);
}
private List<Livro> construirListaDeLivrosDeInformatica() {
return Arrays.asList(new Livro(1L, “Java 1”, “123”, “José”,
new BigDecimal(“110.0”), “Informática”),
new Livro(2L, “Java 2”, “456”, “João”, new BigDecimal(“73.40”),
“Informática”));
}
}
Referências
t 5FTU%SJWFO5%%BOE"DDFQUBODF5%%GPS+BWB%FWFMPQFSTo-BTTF,PTLFMB
t i1SPHSBNBEPSFT QSPmTTJPOBJT FTDSFWFN UFTUFTw IUUQCMPHGSBHNFOUBMDPN
CSQSPHSBNBEPSFTQSPmTTJPOBJTFTDSFWFNUFTUFTQPOUPmOBM
t 8PSLJOH&õFDUJWFMZXJUI-FHBDZ$PEFo.JDIBFM'FBUIFSTo
t (VJB QBSB FTDSJUB EF DØEJHP UFTUÈWFM IUUQHPPHMFUFTUJOHCMPHTQPUDPNCZ
NJLPIFWFSZTPZPVEFDJEFEUPIUNM
t 6TBOEPP)42-%#FNNFNØSJBQBSBFTDSJUBEFUFTUFTIUUQXXXUIFTFSWFSTJEFDPN
UUBSUJDMFTBSUJDMFUTT M6OJU5FTUJOH
t 5FTUFTEF%"0TDPN)42-%#IUUQHDCMPHCSUFTUFTDPNKVOJUITRMEC
t #FIBWJPSESJWFO%FWFMPQNFOUIUUQQUXJLJQFEJBPSHXJLJ#FIBWJPS@%SJWFO@%FWFMPQment
Download

Automatização de testes de persistência com FIT, DBUnit