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