2 (boas) formas de testar seu código JavaMail

318977117_8708c74907_m.jpgUma constante no desenvolvimento de qualquer aplicação web é o envio de e-mails. Ah, o usuário se registrou? E-mail nele! O usuário trocou de senha? E-mail nele! O pedido foi recebido com sucesso? Em… chega, vai!

Sem sombra de dúvidas, a API mais usada para resolver essa questão é o JavaMail. Também existem algumas opções, como o Spring Mail e Commons Email, mas, no fim das contas, é o próprio JavaMail quem faz o serviço sujo (tais APIs são feitas sobre o próprio JavaMail).

Apesar de dar conta do recado, a referida API conta com alguns problemas bastante chatos, dentre os quais eu destaco o design de suas classes e interfaces (existem interfaces?), :P que parecem fazer o possível para dificultar a testabilidade dos seus “clientes”.

Entendendo o problema

Uma técnica de testes bastante utilizada atualmente consiste no uso de frameworks para Mocking de objetos. Esses frameworks permitem testar, em unidade, um código que faça uso de quaisquer recursos externos e que são, por natureza, difíceis de testar. Classes que necessitam acessar recursos de infraestrutura, por exemplo, são ótimas candidatas para a criação de testes apoiados no uso de objetos Mock.

O grande problema é que, para que tais frameworks possam ser usados, o código a ser “mockado” tem de seguir algumas regrinhas de codificação. Por exemplo, alguns frameworks não conseguem “mockar” uma classe concreta; outros por sua vez, conseguem “mockar” classes concretas desde que elas não sejam finais.

Bom, só para resumir, o ideal seria que o código todo — tanto das classes de infraestrutura quanto das classes que fazem uso das primeiras — seja orientado a interfaces. Isso é necessário pois grande parte dos frameworks Java para Mocking de objetos se apoiam na API Proxy para fazer seu trabalho e, como muitos já devem saber, não é possível criar proxies de classes contretas através do uso dessa API.

Para ilustrar melhor o que estou tentando dizer, segue um exemplo de código que envia um e-mail com a API JavaMail:

  1.  
  2. Properties props = System.getProperties();
  3. props.put("mail.smtp.host", "smtp.middlenet.com");
  4. Session session = Session.getDefaultInstance(props);
  5.  
  6. MimeMessage message = new MimeMessage(session);
  7. message.setFrom(new InternetAddress("Frodo", "frodo@theshire.com"));
  8. message.addRecipient(RecipientType.TO, new InternetAddress("Gandalf", "gandalf@asskickers.com"));
  9. message.setSubject("Be careful");
  10. message.setText("Sauron is looking for the One Ring!");
  11.  
  12. Transport.send(message);
  13.  

Holy crap! Veja, por exemplo, que o objeto Session é obtido através da chamada ao método estático getDefaultInstance(); de modo semelhante, temos também a chamada ao método estático send(), na última linha do código. Ah claro, sem contar que a classe Session é final! Como podemos fazer para testar o que vai para e o que volta da API JavaMail?

Uma solução…

São em momentos como esse que precisamos dar uma improvisada. :P Mesmo não sendo possível testar o código mostrado anteriormente com um framework para Mocking de objetos, ainda existem algumas opções disponíveis.

Uma opção bastante interessante é a utilização de um servidor SMTP “de mentirinha”, para o qual as mensagens são enviadas. Devo dizer que, após usar essa estratégia para testar códigos de envio de e-mail, eu achei até mais interessante do que o fazer através de mocks (veremos mais adiante que isso, de certo modo, é possível de ser feito). Dessa forma, podemos verificar o que acontece quando o código “conversa” com um servidor, o que na minha opinião torna o teste um pouco mais confiável.

A boa notícia é que não precisamos implementar um servidor SMTP do zero! Uma opção de servidor como esse é o SubEthaSMTP Wiser (!). Com ele, podemos subir um servidor SMTP basicão numa porta qualquer. Então, basta configurar nossa classe de envio de e-mail de modo que tal servidor seja usado no lugar do servidor de produção. As mensagens enviadas a esse servidor podem ser consultadas e verificadas através de asserções no código de testes.

Usando o SubEthaSMTP Wiser com o Maven

Se você usa o Maven, então basta adicionar a dependência no seu POM para tornar as classes do SubEthaSMTP Wiser disponíveis no seu ambiente de testes:

  1.  
  2. <dependency>
  3.     <groupId>org.subethamail</groupId>
  4.     <artifactId>subethasmtp-wiser</artifactId>
  5.     <version>1.2</version>
  6.     <scope>test</scope>
  7. </dependency>
  8.  

Talvez você veja uma mensagem de erro indicando que a dependência javax.activation:1.0.2 não foi encontrada. Se isso acontecer, você deverá baixar essa versão do site da Sun e fazer a instalação manualmente no seu repositório local. (o Maven informa qual comando rodar para fazer essa instalação).

Exemplo de uso

Usar esse servidor é uma tarefa bem simples. A maior dificuldade é configurar a nossa classe de envio de e-mails para se conectar nesse servidor em vez do servidor de produção. Veja só:

  1.  
  2. EmailMessage message = … ; // objeto com os dados da mensagem
  3.  
  4. /* inicia o servidor */
  5. Wiser server = new Wiser();
  6. server.setPort(2500);
  7. server.start();
  8.  
  9. /* configura o cliente */
  10. Properties prop = System.getProperties();
  11. prop.setProperty("mail.smtp.host", "localhost");
  12. prop.setProperty("mail.smtp.port", "2500");
  13. session = Session.getDefaultInstance(prop);
  14.  
  15. EmailSender sender = new JavaMailEmailSender(session); // minha classe
  16. sender.send(message); // o parâmetro contém os dados de envio e corpo da mensagem
  17.  
  18. server.stop(); // pára o servidor
  19.  
  20. /* verifica o que foi enviado */
  21. assertEquals(1, server.getMessages().size());
  22. assertEquals("Sauron is looking for the One Ring!", server.getMessages().get(1).getMimeMessage().getContent());
  23.  

Nas primeiras linhas iniciamos o servidor. Em seguida, configuramos a Session do JavaMail para que ela se conecte ao nosso servidor “de mentirinha”. A partir desse ponto, tudo funciona como de costume, com a vantagem de podermos realizar asserções para verificar se as mensagens chegaram ao servidor da forma esperada. Isso é especialmente útil quando utilizamos algum tipo de processador de templates durante o envio das mensagens, permitindo que possamos testar o que exatamente chegou ao servidor.

Para quem não gostou dessa abordagem…

Existe ainda uma outra opção que podemos utilizar para testar classes que fazem uso do JavaMail. Trata-se de um projeto chamado mock-javamail que, ao contrário do SubEthaSMTP Wiser, não usa um servidor SMTP “de mentirinha” para receber as mensagens; o que ele faz é configurar o JavaMail em runtime de modo que as mensagens enviadas sejam armazenadas em memória.

Usando o mock-javamail com o Maven

Primeiramente precisamos adicionar o repositório Maven do java.net:

  1.  
  2. <repositories>
  3.     <repository>
  4.         <id>java.net2</id>
  5.         <url>http://download.java.net/maven/2</url>
  6.     </repository>
  7. </repositories>
  8.  

Feito isso, basta declarar a dependência:

  1.  
  2. <dependency>
  3.     <groupId>org.jvnet.mock-javamail</groupId>
  4.     <artifactId>mock-javamail</artifactId>
  5.     <version>1.3</version>
  6. </dependency>
  7.  

Exemplo de uso

Este projeto é mais simples de se utilizar que o SubEthaSMTP Wiser justamente pelo fato de não precisarmos iniciar um servidor SMTP. Isso fica evidente no exemplo abaixo, que é uma adaptação do código anterior:

  1.  
  2. EmailMessage message = … ; // objeto com os dados da mensagem
  3.  
  4. /* configura o cliente */
  5. Properties prop = System.getProperties();
  6. prop.setProperty("mail.smtp.host", "localhost");
  7. prop.setProperty("mail.smtp.port", "2500");
  8. session = Session.getDefaultInstance(prop);
  9.  
  10. EmailSender sender = new JavaMailEmailSender(session); // minha classe
  11. sender.send(message); // o parâmetro contém os dados de envio e corpo da mensagem
  12.  
  13. /* verifica o que foi enviado */
  14. assertEquals(1, Mailbox.get("gandalf@asskickers.com").size());
  15. assertEquals("Sauron is looking for the One Ring!", Mailbox.get("gandalf@asskickers.com").get(0).getContent());
  16.  

Conclusão

Primeiramente, demos uma olhada no SubEthaSMTP Wiser, um servidor SMTP “de mentirinha” que nos permite verificar exatamente como a mensagem chegaria caso tivesse sido enviada para um servidor SMTP “normal”.

No entanto, esse negócio todo pode parecer gambiarra para alguns. Por isso, o framework mock-javamail foi apresentado como alternativa ao primeiro. Este, ao contrário do SubEthaSMTP Wiser, permite a verificação das mensagens enviadas pelo JavaMail sem a necessidade de um servidor SMTP.

De qualquer forma, as duas alternativas cumprem seu papel, que é ajudar a solucionar o problema da péssima testabilidade da API JavaMail.

UPDATE: disponibilizei o projeto para download na página de aplicações aqui do blog.

Foto por: Narisa.

Tags: , , , , , , , , ,