Testes Automatizados. : : COLUNA Cinto de Utilidades: : - PDF
Testes Automatizados. : : COLUNA Cinto de Utilidades: :
Download "Testes Automatizados. : : COLUNA Cinto de Utilidades: :"
Andreia Ramalho Tuschinski
1 : : COLUNA Cinto de Utilidades: : Alexandre Gazola / é 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, desenvolve em Java desde 2003 e possui as certificações SCJP e SCWCD. É articulista e editor-técnico da revista MundoJ, além de manter um blog em wordpress.com. Também está aprendendo a tocar violão e é um entusiástico estudante de idiomas. Com a popularidade dos métodos ágeis nos últimos anos, boas práticas como o uso de testes automatizados e integração contínua já são de conhecimento de boa parte dos desenvolvedores profissionais. Detecção precoce de bugs, feedback rápido e melhoria contínua no design das aplicações são apenas alguns benefícios que podemos citar. Apesar das vantagens, ainda existem os que preferem utilizar apenas testes manuais ad-hoc ou, pior ainda, a filosofia do se compilou, então está funcionando! E você? Desenvolve software com o uso de testes automatizados? Ainda não? Então, continue lendo, pois neste artigo vamos passar por algumas das principais ferramentas utilizadas para a plataforma Java dentro do contexto de um processo de desenvolvimento orientado a testes. Testes Automatizados ocê recebeu a incumbência de implementar a funcionalidade ABC do novo portal Web da empresa. O que você faz? Pensa um pouco no design, começa a escrever código e no final faz alguns testes manuais para confirmar se tudo está correto. A equipe de qualidade (se houver uma) encontra alguns poucos problemas, você corrige e a funcionalidade ABC é, então, implantada em produção. O portal começa a fazer bastante sucesso entre seus usuários e a empresa aproveita o bom momento para solicitar a inclusão da funcionalidade EFG. Você recebe a solicitação e parte para o desenvolvimento. Contudo, para a inclusão desta funcionalidade, você se dá conta que terá que fazer uma refatoração importante no sistema, com impactos nas funcionalidades já desenvolvidas. Com um frio da barriga, você realiza as transformações necessárias no código, faz alguns testes básicos (manuais) e se dá por satisfeito para prosseguir com a implementação da nova funcionalidade. Após entregar o software para a equipe de qualidade testar, você começa a ver diversos bugs sendo cadastrados numa ferramenta de bug tracking; com o detalhe de que vários destes bugs dizem respeito à funcionalidade ABC, que já estava funcionando. 27
2 : : : : A história poderia ter sido diferente se a funcionalidade tivesse sido desenvolvida com testes automatizados, que constituem uma boa rede de proteção contra a regressão do sistema 1. A existência de testes automatizados também é pré-requisito para a refatoração constante do sistema (de forma segura). Esta, por sua vez, também é uma prática importante para que o design da aplicação evolua e melhore continuamente de forma a melhor acomodar as novas funcionalidades. É praticamente impossível construir um sistema OO usando um modelo de domínio rico (que é constantemente aperfeiçoado) sem aplicar refatoração constante. Daí a importância de se ter uma boa suíte de testes automatizados. Outro benefício relacionado ao uso dos testes também pode ser obtido quando usamos o desenvolvimento orientado a testes (TDD Test-driven Development). Neste estilo, começamos a desenvolver uma funcionalidade primeiramente escrevendo um teste automatizado, para só então implementar o código que deverá satisfazer ao teste. Uma vez que o teste esteja passando, procedemos ao trabalho de melhorar o código produzido (i.e. refatoração) visto que já dispomos de um mecanismo de proteção para nos fornecer a devida segurança. Nesta maneira de se desenvolver, em vez de aplicar o tradicional ciclo design-codificação-testes, ao receber uma nova funcionalidade, aplicamos testes-codificação-design. Neste artigo, iremos fornecer uma noção geral sobre TDD, com alguns exemplos básicos de ferramentas importantes utilizadas no mundo Java. Falaremos sobre os ciclos de feedback interno e externo no desenvolvimento orientado a testes, contextualizandoos na prática por meio das ferramentas JUnit, Mockito, DbUnit (com HSQLDb), JBehave, Selenium e EclEmma. Escolhemos a plataforma Java como exemplo para as ferramentas por ser esta de conhecimento da maioria dos leitores. Não obstante, o processo geral apresentado e as ferramentas mencionadas podem ser facilmente mapeados para outras plataformas. Ressaltamos que o propósito aqui não é de nos aprofundarmos em nenhuma técnica ou ferramenta, mas apenas expor o leitor ao processo de desenvolvimento com testes e algumas das ferramentas de suporte. Após ler o artigo, encorajamos fortemente o leitor a estudar cada uma das ferramentas citadas. Ao longo do texto e ao final, referenciamos alguns dos diversos artigos já publicados sobre esse tema na revista MundoJ, além de relacionarmos alguns bons livros sobre o assunto na seção de referências. Ciclos de feedback Uma característica que permeia as práticas de desenvolvimento de software ágil é o estabelecimento de ciclos de feedback rápidos e contínuos ao longo do projeto (pense, por exemplo, em programação em par, stand-up meetings, quadros kanban, cliente sempre presente etc.). Isso também é algo bem evidente quando desenvolvemos software utilizando testes automatizados, especialmente quando aplicamos o TDD. Nesta técnica, primeiramente escrevemos um teste automatizado para o pedaço de funcionalidade a ser desenvolvida, e só depois, então, é que implementamos a funcionalidade propriamente dita. Dessa forma, temos um critério para sabermos quando realmente acabamos a implementação; conseguimos saber se o código que acabou de ser criado tem uma boa qualidade, ou seja, se ele atende nossas expectativas expressas por meio dos testes automatizados. Para cada nova funcionalidade sendo criada, temos feedback instantâneo e localizado a respeito do seu grau de corretude. Além disso, o número de testes para o software cresce continuamente, provendo uma rede de segurança que ajuda a proteger o sistema contra a introdução de novos bugs (os chamados testes de regressão). Vale frisar que o TDD é primariamente uma técnica de design (com o grande benefício colateral de nos prover uma boa suíte de testes de regressão), pois o exercício de escrever o teste antes nos força a modelar o sistema de tal forma que ele seja fácil de testar, o que por sua vez acaba desencadeando um design com baixo acoplamento. Testes automatizados podem nos ajudar a garantir tanto qualidade externa quanto interna num sistema de software. Qualidade externa está relacionada às funcionalidades do software em si, perceptíveis pelo usuário da aplicação (o sistema atende à necessidade de negócio de seus usuários de forma satisfatória?). Qualidade interna está relacionada ao código do sistema (o código é legível e de fácil manutenção?). Para endereçar a qualidade externa do sistema, podemos fazer uso de testes de aceitação (usando uma abordagem de Acceptance TDD ou de Behavior-driven Development) e para endereçar a qualidade interna do sistema podemos fazer uso de testes de unidade (usando uma abordagem de TDD). Essas duas abordagens, utilizadas em conjunto, podem nos prover valiosos ciclos de feedback ao longo do desenvolvimento de um sistema de software. Ao longo do artigo, vamos mostrar algumas ferramentas Java importantes para desenvolvermos software com qualidade (tanto do ponto de vista interno quanto externo). Para isso, vamos utilizar a aplicação fictícia de exemplo do quadro 1 como ponto de partida 2. Desenvolvimento com testes Considerando a aplicação descrita no quadro 1, vamos mostrar como podemos acrescentar novas funcionalidades a ela por meio do desenvolvimento orientado a testes. Neste processo, introduziremos algumas das ferramentas de suporte. Vamos supor que nosso cliente priorizou a seguinte história para ser desenvolvida na próxima iteração: Como músico Desejo enviar uma nova tablatura Para compartilhá-la com outras pessoas Como pode ser notado, a história não diz muita coisa que possa nos ajudar em termos de implementação 3. Precisamos conversar com o cliente sobre o que de fato o sistema deve fazer para satisfazer essa história, ou seja, quais seus critérios de aceitação ou cenários de uso. Numa conversa com o cliente, levantamos o seguinte cenário 4 : 1 Escrevi poderia ter sido diferente, pois a qualidade desta rede de proteção está intimamente relacionada à qualidade dos testes automatizados escritos. 2 A inspiração para a aplicação de exemplo foi obtida a partir do artigo Behaviour Driven Development: By Example, de Ryan Greenhall (ver referências). 3 De fato, em Programação Extrema (XP), histórias são como lembretes para conversas futuras com o cliente. 4 Em nosso exemplo, desprezamos a informação sobre em qual oitava as notas musicais se encaixam. 28
3 TABShare O TABShare é uma aplicação na Web para compartilhamento de tablaturas musicais. De maneira simplificada, uma tablatura é uma notação gráfica utilizada para representar as posições dos dedos da mão esquerda em instrumentos de cordas (ex.: violão, guitarra etc.). É uma alternativa mais simples ao uso de uma partitura. Uma tablatura pode ser facilmente representada em caracteres ASCII, como no exemplo seguinte (introdução simplificada da canção Rompendo em fé, de Edson e Ana Feitosa): e B G D A E A ideia do TABShare é permitir a seus usuários o compartilhamento de tablaturas. Os usuários podem, por exemplo: enviar novas tablaturas, visualizar as notas musicais representadas por uma tablatura e avaliar a qualidade das tablaturas enviadas por outros usuários. As tablaturas são armazenadas em uma tabela de banco de dados relacional mapeada para uma classe correspondente por meio do Hibernate. Os atributos de uma tablatura são conteúdo, título e descrição. Quadro 1. Aplicação de exemplo: TABShare. Dado que estou logado no sistema na página de envio de tablaturas quando envio a seguinte tablatura e B G D A E e informo como título Rompendo em fé e descrição Melodia introdutória. Então o sistema informa que a tablatura foi enviada com sucesso e as notas que a compõem são: D-C#-B-C#-D-A-D-C#-B-C#-D-F#-D-E-F#-G-F#-G-F#-E-F#-E-D-D Para ilustrar os ciclos de feedback interno e externo, nosso processo de desenvolvimento se dará da seguinte forma: 1) escrevemos testes de aceitação (testes de aceitação falhando); 2) escrevemos testes de unidade (testes de unidade falhando); 3) escrevemos código de produção para satisfazer os testes de unidade (testes de unidade passando); 4) melhoramos o código de produção por meio de refatoração (testes de unidade passando por consequência, testes de aceitação quase passando); 5) escrevemos o código final da aplicação necessário (GUI etc.) para satisfazer os testes de aceitação (testes de aceitação passando). Observação: os passos 2, 3 e 4 são executados em loop, com um teste de cada vez até que todos os testes de unidade sejam concluídos. Um loop externo análogo ocorre com os testes de aceitação. Vejamos agora a execução deste processo de desenvolvimento passo-a-passo. Ciclo externo: testes de aceitação com JBehave e Selenium De posse do cenário de uso exibido anteriormente, já temos condições de partir para a implementação. Neste ponto, podemos utilizar a ferramenta JBehave para expressar este cenário como um teste executável. O JBehave é um framework para desenvolvimento de software usando Behavior-driven Development (BDD), cuja ideia é expressar testes sob a forma de cenários escritos em linguagem natural (ele possui suporte built-in a diversas línguas, entre elas o português). De maneira proposital, o cenário da nossa aplicação exibido anteriormente já está no formato do JBehave (o formato consiste basicamente em usar as palavraschave Dado que, Quando e Então ). Colocamos esta descrição num arquivo chamado envio_de_tablatura.story. Agora precisamos de uma classe Java que conterá o código que irá realizar a tradução do texto nas ações correspondentes sobre o sistema. A Listagem 1 exibe um exemplo desse tipo de código. O mapeamento é realizado por meio das anotações do correspondentes aos trechos de texto com as 29
4 : : : : palavras-chave mencionadas). Além disso, precisamos de outra classe, exibida na Listagem 2, responsável por executar a especificação. O artigo Behaviour-Driven Development em Java na prática, com JBehave da MundoJ 44 explica em detalhes a filosofia do BDD, juntamente com o JBehave. O interessante do JBehave é fazer essa ponte entre os requisitos do sistema e sua implementação, constituindo, até certo ponto, uma espécie de documentação executável que pode ser facilmente validada pelo cliente ou especialista de domínio. Como temos uma aplicação Web, pode ser interessante que nossos testes de aceitação façam uso da aplicação usando sua interface gráfica no browser. Para isso, podemos usar o Selenium, ferramenta utilizada para realizar testes funcionais de aplicações Web. Ele pode ser instalado como plugin para o Firefox e usado para gravar num script as ações que um usuário executa numa aplicação, com opções de inserção de pontos de verificação para que se possa determinar se o aplicativo se comporta da maneira esperada (do ponto de vista funcional, com relação ao conteúdo das páginas exibidas). Uma vez gravado o script, o plugin oferece a funcionalidade de exportação como teste JUnit (mais sobre esta ferramenta adiante), o qual pode, então, ser integrado à suíte de testes de uma aplicação. A API do Selenium é bastante enxuta, de forma que não é nem tão complicado de escrevermos o código diretamente em Java sem precisar realizar a gravação e a exportação do teste. De fato, como programamos com o teste primeiramente, ainda nem dispomos da interface gráfica da aplicação para gravar os testes. Quando usamos uma API como o Selenium para testar uma aplicação através de sua interface gráfica, é bastante recomendável empregarmos o padrão Page Objects. A ideia do padrão é simples: cada página da aplicação é modelada por meio de uma classe responsável por oferecer as operações que podem ser realizadas na respectiva página. A classe que modela a página encapsula as chamadas à API do Selenium, simplificando a manutenção (pode-se reaproveitar a página em diversos testes) e tornando mais expressivos os cenários de escritos (como pode ser visto no código de teste da Listagem 1). A Listagem 3 exibe uma ideia da classe da página utilizada no teste. Neste código, já exibimos a interação que imaginamos que será feita com a página apenas para que o leitor tenha uma noção de como é a API do Selenium. Contudo, num ambiente de desenvolvimento real, o código de interação com o Selenium poderia ser adicionado às classes das páginas ao final do ciclo interno de desenvolvimento (explicado a partir da seção seguinte). Em particular, o código poderia ser gerado automaticamente via plugin do navegador após a confecção da interface gráfica. Para detalhes de como usar o Selenium com JBehave, conferir o artigo Qualidade através de testes funcionais com Selenium, JBehave e Maven da MundoJ 46. Listagem 1. Especificação de cenário de uso usando JBehave. public class EnvioDeTablaturaSteps { private PaginaEnvioTablatura estou logado no sistema na página de envio de tablaturas ) public void logarnosistema() { paginaenvio = paginalogin.logar( usuario, senha envio a seguinte tablatura $tablatura e informo + como titulo $titulo e descricao $descricao ) public void enviodetablatura(string conteudo, String titulo, String descricao) { paginaenvio.enviartablatura(conteudo, titulo, descricao) o sistema informa que a tablatura foi enviada com sucesso + e as notas que a compõem são: %s ) public void tablaturaenviadacomsucesso(string notas) { assertequals(notas, paginaenvio.getresultado()); Listagem 2. Classe para executar a especificação. public class EnvioDeTablaturaStory { public Configuration configuration() { return new MostUsefulConfiguration().useStoryLoader( new LoadFromClasspath(this.getClass())).useStoryReporterBuilder(newStoryReporterBuilder().withDefaultFormats().withFormats(Format. CONSOLE, public List<CandidateSteps> candidatesteps() { return new InstanceStepsFactory(configuration(), new EnvioDeTablaturaSteps()).createCandidateSteps(); Mesmo que não seja possível automatizar os testes funcionais de sua aplicação com Selenium, pode ser interessante adotar o JBehave para o desenvolvimento de testes de integração, obtendose o excelente benefício da descrição simplificada dos testes em cenários executáveis. 30
5 Listagem 3. Classe de página para envio de uma nova tablatura com Selenium. public class PaginaEnvioTablatura extends Pagina { // detalhes da API do Selenium encapsulados na classe pai public enviartablatura((string conteudo, String titulo, String descricao) { selenium.open( / ); selenium.type( conteudo, conteudo); selenium.type( titulo, titulo); selenium.type( descricao, descricao); selenium.click( btnenviar ); selenium.waitforpagetoload( ); asserttrue(selenium.istextpresent( Tablatura enviada com sucesso )); Ao executar o código da Listagem 2 (usando o JUnit, mais sobre esta ferramenta adiante), o JBehave nos informará que os três passos da especificação estão pendentes. Esse comportamento é óbvio, visto que não temos nada implementado. De qualquer forma, com este teste em mãos, temos um critério de avaliação quanto ao progresso da implementação da funcionalidade do ponto de vista do usuário externo. Agora, é hora de entrarmos no ciclo interno de desenvolvimento da funcionalidade usando TDD. Testes de unidade com JUnit O TDD está baseado no ciclo red-green-refactor, outra forma de se referir ao ciclo teste-codificação-design, que explicamos anteriormente, usando como referência as cores da barra de resultado exibida por ferramentas de execução de testes de unidade, como o JUnit. Quando algum teste falha, é exibida uma barra vermelha; quando todos os testes passam é exibida uma barra verde. A Listagem 3 exibe um trecho de código com um primeiro teste de unidade para o sistema. A função do teste, expressa pelo seu nome, é verificar a corretude do método getnotas(). Aqui usamos a ferramenta JUnit para escrever os testes automatizados. O JUnit é extremamente popular, já vem com as principais IDEs Java (Eclipse, NetBeans etc.) e é bastante simples de usar. Basta criar uma classe Java comum e anotar os métodos que devem ser executados como testes com a Tendo um método com esta anotação, tudo o que é necessário fazer é escrever o código que exercitará a funcionalidade desejada e, então, especificarmos as assertivas desejadas. No código da Listagem 4, criamos uma nova tablatura, chamando o método getnotas() e verificando se o resultado que ele retorna é igual à string com as notas esperadas. Você pode estar se perguntando onde está a classe Tablatura. A resposta: ela ainda não existe! Estamos escrevendo primeiramente o teste, fazendo o que é chamado de programação por intenção, que consiste em escrevermos um código da forma como gostaríamos de usá-lo antes mesmo de ele existir (este é o pilar central do TDD). Escrever código assim nos força a refletirmos sobre o design do sistema, focando nas suas interfaces e na facilidade de uso, isto é, no quê deve ser feito e não no como fazer. Esse processo fatalmente nos leva a designs menos acoplados, com interfaces que expressam de forma mais clara a sua intenção. O próximo passo é fazer o código compilar por meio da criação da classe Tablatura com o método getnotas() (o Eclipse pode ser usado para fazer isso automaticamente, basta pressionar <Ctrl> + 1 seguido de <Enter> na linha com erro de compilação para que ele gere o código necessário [com implementação vazia]). Com o código compilando, podemos executar o teste pelo Eclipse e ver o teste falhar (barra vermelha!), pois o método getnotas() ainda não faz nada (simplesmente retorna null). Com isso, já se pode implementar de fato o método getnotas() da maneira mais simples com a qual se consiga satisfazer ao teste que está falhando. Com isso feito corretamente, o teste deverá passar (barra verde!). Agora, com o teste passando, procedemos ao último passo do ciclo do TDD, removendo duplicação e tornando o código o melhor possível (refatoração!). Listagem 4. Testes de unidade para a classe Tablatura. public class TablaturaTest public void getnotasok() { String conteudo = e B G D A E ; String titulo = Rompendo em fé ; String descricao = Melodia introdutória ; Tablatura tablatura = new Tablatura(conteudo, titulo, descricao); assertequals( D-C#-B-C#-D-A-D-C#-B-C#-D-F#-D-E-F#-G-F#-G-F#-E- F#E-D-D-, tablatura.getnotas()); // outros métodos de testes exercitando diferentes cenários 31
6 : : : : Ao longo desse processo, novos cenários de testes deverão ser implementados para o método getnotas() bem como outros testes para os demais métodos que se façam necessários para a classe Tablatura. Após esse processo, vamos assumir que chegamos ao código da Listagem 5. Agora, olhando a implementação do método getnotas(), podemos notar que existe a instanciação de um objeto do tipo Tablatura- Parser, responsável por fornecer quais são as casas pressionadas no braço do violão dado um conteúdo de tablatura (deixamos o código para TablaturaParser como exercício para o leitor). Sendo assim, o leitor pode perceber que quando escrevemos um teste para o método getnotas(), o teste também estará testando métodos da classe TablaturaParser e, portanto, deixou de ser um teste de unidade (cujo objetivo é testar métodos de uma classe isoladamente). Bom, aqui, neste exemplo didático, talvez não haja grandes problemas nisso. Porém, se a classe colaboradora (neste caso, TablaturaParser) fosse uma classe de implementação custosa, difícil de instanciar ou dependesse de recursos externos que não estivessem sob fácil controle (ex.: banco de dados, rede etc.) seria bastante difícil escrever um simples teste de unidade. Por isso, o recomendável é utilizarmos injeção de dependência para que possamos substituir os objetos colaboradores por objetos que possam tornar os testes de unidade possíveis. A Listagem 6 exibe o código da Tablatura usando injeção de dependências para o parser colaborador. Para uma boa introdução ao desenvolvimento com testes automatizados e JUnit, conferir o artigo Testes Unitários para Camadas de Negócios no Mundo Real. MundoJ 23. Listagem 5. Código de produção para a classe Tablatura. public class Tablatura { private String conteudo; private String titulo; private String descricao; public Tablatura(String conteudo, String titulo, String descricao) { this.conteudo = conteudo; this.titulo = titulo; this.descricao = descricao; public String getnotas() { StringBuilder resultado = new StringBuilder(); TablaturaParser parser = new TablaturaParserImpl(); List<Casa> casas = parser.parse(descricao); for (Casa casa : casas) { int numero = casa.getnumero(); String corda = casa.getcorda(); //... restante do algoritmo para encontrar a nota... resultado.append(nota); resultado.append( - ); return resultado; Listagem 6. Refatoração da classe Tablatura para usar injeção de dependência. public class Tablatura { //.. private TablaturaParser parser; public Tablatura(String conteudo, String titulo, String descricao, TablaturaParser parser) { //... this.parser = parser; // restante do código igual sem a instanciação da dependência Mock objects com Mockito Após a modificação da classe Tablatura para usar injeção de dependência, devemos modificar o teste de unidade para passarmos o parser exigido (na verdade, essa modificação seria feita primeiramente no teste e depois propagada para o código mas vamos seguir nesta sequência didática). Assumindo que programamos por intenção (escrevendo o teste primeiro, pensando no código que gostaríamos de usar), neste ponto nós teríamos condições apenas de derivar a interface TablaturaParser (ainda não temos sua implementação). Para mantermos nosso foco na classe sendo testada unitariamente, podemos gerar uma implementação fake (um stub) para usarmos nos testes ou fazer uso de um framework de mock objects. Mock objects são objetos falsos que normalmente são utilizados em testes automatizados para substituir objetos colaboradores. Diferenciam-se de simples stubs por possuírem funcionalidades de configuração de quais chamadas de método devem ser realizadas no mock, quantas vezes, com quais argumentos e quais valores de retorno, entre outras coisas. Existem diversos frameworks de mock objects para Java, sendo Mockito e EasyMock os mais populares. A Listagem 7 exibe o código do teste refatorado para usar o Mockito e considerando a injeção de dependência de TablaturaParser. Neste código, utilizamos um método de setup anotado anotação do JUnit para indicar que o referido método deve ser executado imediatamente antes da execução de cada método de teste (também existem Este setup é necessário para que o Mockito faça a criação dos mocks para todos os objetos anotados Para mais detalhes sobre o ciclo do TDD com JUnit e Mockito, conferir o artigo Evolução do Design através de Testes e o TDD da MundoJ 41; e para uma boa explicação sobre a diferença de mocks e stubs, ver o artigo Mocks aren t stubs, de Martin Fowler. Testes dos DAOs com DbUnit e HSQLDb Outra ferramenta também importante no cenário de testes automatizados com Java é o DbUnit, cujo objetivo é facilitar o gerenciamento do estado de um banco de dados entre as execuções dos testes. Com ele, podemos especificar em arquivos XML quais os dados que queremos que sejam inseridos em quais tabelas e 32
7 Listagem 7. Teste de unidade usando mock object. public class TablaturaMockito private TablaturaParser public void setup() { // instancia os mocks para os campos anotados public void getnotasok() { //.. igual ao codigo anterior // when(parser.parse(conteudo)).thenreturn(arrays.aslist(new Casa(7, G ), new Casa(6, G ), new Casa(4, G ) /*... demais casas do conteudo... */)); Tablatura tablatura = new Tablatura(conteudo, titulo, descricao, parser); assertequals( D-C#-B-C#-D-A-D-C#-B-C#-D-F#-D-E-F#-G-F#-G-F#-E-F#- E-D-D-, tablatura.getnotas()); verify(parser).parse(conteudo)); ele, então, se encarrega de fazer as devidas atualizações de forma que o estado do banco fique sempre consistente antes ou depois de cada execução de teste. O DbUnit é uma ferramenta que se mostra bastante útil para realizarmos testes em classes DAO (Data Access Object), que são normalmente as classes que acomodam a funcionalidade de acesso a bancos de dados numa aplicação. Testes que envolvem recursos externos, como bancos de dados, também costumam ser bem mais lentos. Para reduzir o overhead nesses tipos de testes, recomendamos o uso do HSQLDb, um sistema de banco de dados mais leve, escrito em Java e que suporta execução em memória. Cabe ressaltar, no entanto, que ainda assim é bastante recomendável que haja outro nível de testes que utilize o sistema de banco de dados o mais próximo possível do que será utilizado em produção. Em nossa aplicação de exemplo, utilizaríamos um banco de dados igual ao real na execução dos testes de aceitação com o JBehave, por exemplo. A Listagem 8 exibe um exemplo do que poderia ser um teste para o nosso DAO de tablaturas, enquanto a Listagem 9 exibe o XML com os dados a serem inseridos/removidos pelo DbUnit (apenas para que o leitor tenha uma noção). Como pode ser visto, o uso do DbUnit consiste em especificar em arquivo XML os dados a serem populados nas tabelas do banco 5, a criação de um objeto DataSet com os dados do XML e a instrução de carga do objeto DataSet no banco de dados HSQLDb em memória no setup do teste. Em código real, normalmente encapsulamos a configuração do DbUnit em uma classe utilitária e a reaproveitamos ao longo dos testes automatizados. O artigo Testes de Unidade para Camadas de Persistência no Mundo Real da MundoJ 24 trata em detalhes o uso do DbUnit para testes de DAOs. Com os testes de unidade e integração passando, completamos um ciclo interno de feedback. Está na hora de concluirmos a funcionalidade, satisfazendo o teste de aceitação. Listagem 8. Teste de integração de DAO usando DbUnit. public class TablaturaDAOTest { private TablaturaDAO public void setup() { Session session = setupdb(); dao = new TablaturaDAOImpl(session); private Session setupdb() { Class.forName( org.hsqldb.jdbcdriver ); Connection jdbcconnection = DriverManager.getConnection( jdbc:hsqldb:mem:testdb, sa, ); IDatabaseConnection connection = new DatabaseConnection( jdbcconnection); IDataSet dataset = new FlatXmlDataSet(new File( dados_tablatura_dao_test.xml )); DatabaseOperation.CLEAN_INSERT.execute(connection, dataset); // codigo para obter session do public void getobtertablaturasportom() { List<Tablatura> tablaturas = dao.obtertablaturasportom( G ); // realizar os devidos asserts baseado no conteúdo do banco de dados Listagem 9. Exemplo de arquivo com dados de testes para DbUnit. <?xml version= 1.0 encoding= UTF-8?> <dataset> <tablatura id= 1 titulo= Musica X descrição= Intro X conteudo= /> <tablatura id= 2 titulo= Musica Y descrição= Intro Y conteudo= /> <tablatura id= 3 titulo= Musica Z descrição= Intro Z conteudo= /> </dataset> Concluindo o ciclo externo de testes Após os diversos ciclos internos de TDD (red-green-refactor), estamos prestes a atender nosso teste de aceitação, mostrado no início do artigo, na Listagem 1. Para isso, ainda se torna necessária a criação da interface gráfica da aplicação (GUI) e a configuração correta do sistema de banco de dados semelhante ao utilizado em produção. Tendo feito isso, estamos prontos para executar o teste de aceitação e recebermos a barra verde confirmando o sucesso da implementação 6. 5 As tabelas para o banco de teste HSQLDb normalmente são automaticamente geradas pelo Hibernate por meio do uso do parâmetro hibernate.hbm2ddl. auto=true quando a EntityManagerFactory é criada. Para tabelas não mapeadas, a criação da tabela deve ser feita manualmente antes de o DbUnit tentar inserir os dados. 6 Evidentemente, cada historia do sistema seria implementada com diversos testes de aceitação e de unidade, com diferentes cenários e não apenas com um único teste como exibimos aqui por propósitos didáticos. 33
8 : : : : Testes no mundo real Tendo mostrado esta forma interessante de se desenvolver software com testes, vamos voltar agora ao mundo real. Como assim?! você pergunta. Você me faz ler este artigo até aqui para dizer que a realidade é outra?. Calma, amigo leitor. Esta seção é mais para que você se conscientize do fato de que no seu ambiente pode não ser possível aplicar as coisas da maneira ideal (se é que isso existe em desenvolvimento de software). Em projetos reais, nem sempre é possível ou factível aplicarmos na íntegra o processo de desenvolvimento com testes que apresentamos neste artigo (com ambos os ciclos de feeback externo e interno mencionados). Pessoas inexperientes na técnica, processos de desenvolvimento burocráticos, pressões de prazo 7, o tipo de sistema sendo desenvolvido (de repente, pode ser um sistema legado) ou da plataforma sendo utilizada (alguém aí desenvolve em COBOL ou NATURAL?) são todos fatores que podem representar empecilhos a esse modelo de desenvolvimento. Cabe a você adaptá-lo para a sua realidade de forma a obter o máximo dos benefícios. De qualquer forma, se o seu ambiente possui as ferramentas necessárias, encorajamos fortemente o leitor para que se esforce para utilizar testes de unidade e integração (envolvendo mais de uma classe, com banco de dados etc.), seja programando estritamente com o teste primeiro ou não (principalmente, se o leitor for inexperiente na técnica). Contudo, aconselhamos que os testes sejam sempre escritos juntamente (imediatamente antes, de preferência, ou imediatamente depois) com o código de produção para que haja os benefícios de feedback instantâneo e localizado, além de código menos acoplado. Sempre programe com o teste em mente, esteja ele já materializado em código como um teste falhando ou não. Por vezes, antes de começar a escrever os testes de unidade, pode ser útil rabiscar alguma ideia do design do sistema. É importante ressaltar que não se trata de fazer BDUF (Big Design Up-front), mas sim apenas de se ter uma noção da estrutura inicial do sistema para ajudar a escrever os primeiros testes. Alguns desenvolvedores só escrevem testes de integração automatizados depois de implementarem a funcionalidade para terem o benefício dos testes de regressão. Esta abordagem possui pelo menos dois problemas: 1) perda de feedback instantâneo e localizado; 2) baixa qualidade em design (i.e. alto acoplamento e baixa coesão). A perda de feedback localizado é evidente, pois quando ocorrem erros em testes de integração, existem diversos pontos a se analisar, além de o feedback ser tardio, gerando maior tempo de correção pela necessidade de se recuperar o contexto. A baixa qualidade em design se manifesta, pois não existe a preocupação em tornar o código testável isoladamente, acarretando em acoplamentos indesejados no sistema. Também é importante estar atento na qualidade dos testes produzidos, para que não seja gerada uma falsa ilusão de segurança. Normalmente, a cada método não trivial de código de produção corresponderá mais de um método de teste de unidade, exercitando diferentes cenários. Além disso, é imprescindível que os testes sejam escritos com a mesma qualidade que o código de produção (evitar duplicação de código!), pois eles terão que ser mantidos, juntamente com o restante do código da aplicação. Vale a pena mencionar uma última ferramenta que pode ser útil para quem não faz uso estrito de TDD ou está no processo de automatização de testes de código legado: o EclEmma. Esta é uma ferramenta de análise de cobertura de testes. Está disponível para ser utilizada diretamente no Eclipse via JUnit e também para ser integrada ao build da aplicação via Ant. A ferramenta indica quais trechos de código de produção estão sendo executados por algum teste, fornecendo um relatório da porcentagem do código coberto por testes. Uma ferramenta deste tipo pode ser útil para derivar novos casos de testes e também servir como uma métrica (bastante limitada, aliás, mas melhor que nada) de como andam os testes automatizados numa aplicação. 100% ou 90% de cobertura 7 A noção inicial que se tem é que o tempo necessário para desenvolver uma funcionalidade com testes automatizados é pelo menos o dobro do que fazê-lo sem os testes. Porém, considerando-se o ciclo de desenvolvimento como um todo, compensa-se esse custo extra inicial com a redução de defeitos e, quando estes são encontrados, com a redução do tempo necessário para corrigi-los (fora os diversos outros benefícios citados neste texto). 34
9 e testes é irreal e desnecessário (às vezes, até prejudicial) para a maioria dos projetos e o leitor deve se policiar para realmente escrever testes que agreguem valor e não simplesmente para aumentar a porcentagem de cobertura. Por fim, não podemos deixar de mencionar que é bastante desejável que a equipe de desenvolvimento utilize um build automatizado para compilação e execução de testes num servidor de integração contínua para que todo o código integrado seja testado continuamente. À medida que o número de testes cresce numa aplicação, pode ficar inviável executar todos os testes de mais alto nível (aceitação ou integração) na máquina local antes de se realizar o commit no repositório de fontes. Nestes casos, é comum o desenvolvedor executar um subconjunto dos testes em sua máquina e deixar para o servidor de integração contínua rodar os testes mais pesados no sistema inteiro. Como uma última observação sobre o assunto, mais importante até do que a ferramenta é a prática de integrar código frequentemente. Muitos aconselham pelo menos um commit no repositório diariamente (muitos desenvolvedores ainda possuem uma resistência quanto a seguir essa recomendação e o desenvolvimento em pequenos passos estimulado pelo TDD é de grande valia para ajudar nisso). Os commits frequentes diminuem o esforço de integração, facilitando a comunicação da equipe. Considerações finais Testes automatizados de software é um assunto extremamente vasto e o propósito aqui neste artigo foi apenas fornecer uma visão geral sobre o assunto, dando ao leitor uma ideia dos principais tipos de ferramentas usadas num processo de desenvolvimento de software orientado a testes. Incentivamos o leitor a que estude a bibliografia de livros e artigos indicados e, principalmente, esforce-se para aplicar as técnicas no seu trabalho diário. No início, será difícil e poderá até parecer um pouco contraproducente, mas, ao longo do tempo, certamente será perceptível a diferença para a qualidade geral do projeto, considerando características externas e internas de qualidade. É tudo uma questão de conhecimento, prática e, principalmente, disciplina. Para Saber Mais do Real. MJ 23 Mundo Real MJ 24 DBUnit e HSQLDB MJ código legado MJ 43 com JBehave MJ 44 nium, JBehave e Maven MJ 46 Steve Freeman e Nat Pryce Java Developers Lasse Koskela Hunt e Dave Thomas Determinando tu algum negócio, ser-te-á firme, e a luz brilhará em teus caminhos. (Jó 22:28) Referências GUJ Discussões sobre o tema do artigo e assuntos relacionados Discuta este artigo com 100 mil outros desenvolvedores em 35
MOCKITO - - Alexandre Gazola (alexandregazola@gmail.com / Twitter: @alexandregazola)
cinto de utilidades mocks_ CRIAÇÃO DE MOCKS COM MOCKITO No último artigo desta coluna, falamos de uma forma geral sobre diversas ferramentas para suporte a testes automatizados. Dentre as ferramentas mencionadas,