Introdução
Testar persistência com ORM não é uma tarefa fácil. O setup de um banco de dados normalmente é trabalhoso e pouco legível para outra pessoa que não o fez. Mudanças no modelo afetam o setup dos testes. Não é possível dissociar os testes das expressões de acesso a dados (hql/criteria) dos testes do próprio mapeamento (anotações e xml’s). Em muitos momentos não sabemos se comportamentos inesperados são culpa de nossas “queries” incompletas, mapeamentos errados ou setup defasados. Apesar da dificuldade, é comum testarmos métodos de acesso a dados, pois percebemos sua importância. No fundo sabemos que não estamos testando apenas persistência, mas sim a regra de negócio pura. HQL e criteria não são linguagens de acesso a dados, mas sim linguagens para manipulação de objetos para realização das regras de negócio.
Estratégias
Mockar um gerenciador de persistência não é algo viável, pois seu setup seria tão complexo como escrever o próprio gerenciador. Fixtures para banco de dados são pouco flexíveis e normalmente focam o modelo físico. Devido a esses problemas, desenvolvedores gastam muito tempo com setup de banco de dados, inserindo registros que configuram um estado para que determinados testes passem ou falhem. Se este setup for criado usando um entityManager, por exemplo, o desenvolvedor terá que se preocupar com estratégia de auto-incremento, ordem de inserção, exceções lançadas, nulabilidade e outras validações do modelo de metadados antes mesmo de pensar no problema inicial. Se for criado via fixture, existem as preocupações com tipos de dados, nome de colunas, etc. Este é exatamente o ponto que o HibernateMock atua, tentando trazer mais simplicidade para teste de persistência em modelos JAVA/ORM .
Funcionamento
O framework instancia e persiste automaticamente toda arvore de objetos a partir da classe passada. Ele realiza a instanciação usando valores padrões ou valores informados no setup. Pode-se configurar user types, enumeradores, tipos primitivos e wrappers, collections, maps, etc. Após a instanciação é realizada toda lógica de inserção, evitando IdentifierGenerationException, TransientObjectException, ConstraintViolationException, PropertyValueException, LazyInitializationException entre outras. Diminui a necessidade de limpar o banco de dados entre os testes, pois ele cuida de inserir registros novos e isolados em toda a árvore de objetos. Agora as alterações no modelo impactam menos os testes, pois não há acesso explicito a API de persistência no setup dos testes. O framework permite que o desenvolvedor teste somente o fluxo de exceção da persistência, pois entidades e dados válidos já são sendo testados quando o método “any” é invocado sem exceções.
Bean Validation
O HibernateMock facilita os testes de modelos anotados com a JSR303 e as extensões do hibernate. Isto significa que são gerados valores que passam na validação automaticamente, e como a arvore é toda válida, os cascades indesejados em nível de teste (@Valid) não atrapalham os testes da entidade em questão. Todos estes benefícios possibilitam o teste de persistência sem ser incomodado pelas validações e o teste de validações sem serem incomodados pela persistência. Para testar validações obtêm-se um objeto persistente válido, altera seu estado para inválido, persiste manualmente e realiza a checagem da exceção.
Setup
//Setup de Aluguel no escopo do modelo
Aluguel aluguel = new Aluguel();
aluguel.setPlaca("KKK1234");
aluguel.setDate(new Date());
hibernateMock.when(Aluguel.class).deliver(aluguel);
Pessoa pessoa = hibernateMock.any(Pessoa.class); //Se for possível navegar de pessoa para qualquer propriedade do tipo Aluguel, a instância de aluguel será como o configurado.
//Setup de Aluguel no escopo de classe
Aluguel aluguel = new Aluguel();
aluguel.setPlaca("KKK1234");
aluguel.setDate(new Date());
hibernateMock.when(Person.class).deliver("alugueis", Arrays.asList(aluguel)); //neste caso somente a propriedade alugueis da classe Pessoa é configurada, as demais propriedades do tipo Aluguel fica default
//Setups de propriedades usando var... args
hibernateMock.when(Aluguel.class).deliver("placa", "KKK1234", "data", new Date());
Podemos fazer um setup para qualquer entidade, não é necessário fazer o setup para a mesma entidade que você requisita pelo “hibernateMock.any”, mas para isso fazer sentido, deve ser possível navegar da entidade que foi criada pelo “any” para a entidade que foi especificado o setup, pois se não houver navegabilidade, ela nem será instanciada.
Caso real
//testar
List<Pessoa> listarPessoasQueAlugaramCarroNoPeriodo(String placa, Date inicio, Date fim){//select pessoa from Pessoa pessoa join pessoa.alugueis aluguel where aluguel.placa = :placa and aluguel.data between :inicio and :fim
}
//teste
public void testListagemDeAluguel() {
//setup
hibernateMock.when(Aluguel.class).deliver("placa", "KKK1234", "data", new Date());
Pessoa pessoa = hibernateMock.any(Pessoa.class); //persiste a árvore inteira
//perform
Date inicio = new Date(System.currentTimeMillis() + 300000);
Date fim = new Date(System.currentTimeMillis() - 300000);
List<Pessoa> pessoas = listarPessoasQueAlugaramCarroNoPeriodo("KKK1234", inicio,
fim);
assertTrue(pessoas.contains(pessoa)); //com equal/hashcode no id
}
//testar
List<Usuario> listarUsuariosComIdadeOuSexo(Integer idade, String sexo){
//select usuario from Usuario usuario where idade = :idade or sexo = :sexo
}
//teste
public void testUsuariosComIdade() {
//setup
hibernateMock.when(Usuario.class).deliver("idade", 27).deliver("sexo", "F"); //prepara dois objetos
List<Usuario> usuarios = hibernateMock.anyList(Usuario.class);
//perform
List<Usuario> usuariosDB = listarUsuariosComIdadeOuSexo(27, "F");
assertTrue(usuariosDB.containsAll(usuarios));
}
Conclusão
Existem frameworks como o DBUnit, DBAssert e ORMUnit que ajudam o teste de persistência, mas não dão a facilidade que o mock-style do HibernateMock oferece. Além disso, existem outras features que podem ser verificadas no site do projeto. Esta ferramenta ajuda a escrever testes que são menos afetados pelo estado do banco, ou seja, importa menos se o banco está sujo ou não. Prefira usar “contains” ao invés de “size” para dar assert em coleções. Apesar de ser um framework novo, atinge seu objetivo de ter o funcionamento semelhante a um framework de mock tradicional, onde as informações do próprio setup são usadas para realizar os asserts.
Sobre o autor:
Célio Vasconcelos Lima é Bacharel em Sistemas de Informação pela UEG e Pós-Graduado em TI pela UNIVERSO e vive desenvolvimento de software desde 2002. Atualmente é arquiteto de software na Data Traffic S/A, Goiânia/GO. Twitter @celiohc, celiovasconcelos@gmail.com.