В данной статье я хочу рассказать об одном очень простом способе тестировать код, который использует Java Mail API.

Трудности в тестировании такого кода следующие:

  1. отправка сообщения осуществляется при помощи статического метода javax.mail.Transport.send, и его сложно “замочить”
  2. для тестирование необходим почтовый сервер, через который будут отправляться сообщения
  3. нужно писать клиента, чтобы сравить то, чтобы было отправлено, с тем, что было получено
  4. если несколько разработчиков одновременно запустят тесты, то на сервер придут дубликаты, чтобы избежать конфликтов, надо усложнять тесты и генерировался какой-нибудь иникальный идентификатор и по нему уже искать сообщения

Можно, конечно, “замочить” javax.mail.Transport.send. Существует много мок-фреймворков, которые позволяют это сделать, но есть способ лучше и проще!

Дело в том, что разработчики Java Mail API предусмотрели возможность расширения путем регистрации своих провайдеров в файле META-INF/javamail.providers. Не буду вдаваться в технические подробность, так как я из не знаю :)

Благо есть на свете люди, которые уже подумали об этом и написали такой мок-провайдер: http://java.net/projects/mock-javamail. Чтобы его спользовать достаточно в classpath добавить mock-javamail.jar и все.

Данный провайдер работает как in-memory почтовый сервер. Это означает, что все отправленные сообщения сохраняются в почтовых ящиках, из которых их можно извлечь. С каждым уникальным почтовым адресом связан один почтовый ящик, который создается автоматически, при отправке сообщения.

Доступ к ящику осуществляется через класс Mailbox. Данный класс отвечает за хранение пришедших сообщений в обычной коллекции и позволяет добавлять/удалять сообщения, получать количество новых сообщений. Кроме того, данный класс имеет два статических фабричных метода, которые возвращают объект почтового ящика по переданному почтовому адресу (если ящика нет, он будет создан). Ниже показан пример вызова:

Mailbox mailbox = Mailbox.get("some@email.com");

Еще один статический метод, который, на мой взгляд, заслуживает внимания: Mailbox.clearAll(). Этот метод удаляет все почтовые ящики. Полезно вызывать перед началом теста, чтобы не мешались данные от предыдущих. Пример использования:

Mailbox.clearAll();

Ниже я приведу пример использования данного провайдера:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>ru.nikisoft.articale</groupId>
	<artifactId>mock-javamail</artifactId>
	<version>1.0.0</version>
	<packaging>jar</packaging>
	<dependencies>
		<dependency>
			<groupId>org.jvnet.mock-javamail</groupId>
			<artifactId>mock-javamail</artifactId>
			<version>1.11</version>
		</dependency>
	</dependencies>
</project>
package ru.nikisoft.article.mockjavamail;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.mail.Address;
import javax.mail.Message;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;

import org.junit.Test;
import org.jvnet.mock_javamail.Mailbox;

public class MailSenderTest
{
	@Test
	public void testSend() throws MessagingException, IOException
	{
		Mailbox.clearAll();
		Mailbox mailbox = Mailbox.get("to@email.com");

		MailSender mailSender = new MailSender("smtp.host", "from@email.com");
		mailSender.send("to@email.com", "Subject: testSend", "Body: testSend");

		assertEquals(1, mailbox.getNewMessageCount());
		assertFromEquals(mailbox.get(0), "from@email.com");
		assertToEquals(mailbox.get(0), "to@email.com");
		assertSubjectEquals(mailbox.get(0), "Subject: testSend");
		assertBodyEquals(mailbox.get(0), "Body: testSend");
	}

	private void assertFromEquals(Message message, String... expectedFrom) throws MessagingException
	{
		Set<String> expectedFromSet = new HashSet<String>(Arrays.asList(expectedFrom));

		for (Address actualFrom: message.getFrom())
		{
			expectedFromSet.remove(actualFrom.toString());
		}

		if (expectedFromSet.size() > 0)
			fail("From should contain these addresses: " + expectedFromSet);
	}

	private void assertToEquals(Message message, String... expectedTo) throws MessagingException
	{
		Set<String> expectedToSet = new HashSet<String>(Arrays.asList(expectedTo));

		for (Address actualFrom: message.getRecipients(RecipientType.TO))
		{
			expectedToSet.remove(actualFrom.toString());
		}

		if (expectedToSet.size() > 0)
			fail("From should contain these addresses: " + expectedToSet);
	}

	private void assertSubjectEquals(Message message, String expectedSubject) throws MessagingException
	{
		String actualSubject = message.getSubject();
		assertEquals(expectedSubject, actualSubject);
	}

	private void assertBodyEquals(Message message, String expectedBody) throws IOException, MessagingException
	{
		String contentType = message.getContentType();

		if (contentType.contains("text/plain"))
		{
			String actualBody = (String) message.getContent();
			assertEquals(expectedBody, actualBody);
			return;
		}

		fail("Unsupported content-type: " + contentType);
	}
}
package ru.nikisoft.article.mockjavamail;

import java.util.Properties;

import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

public class MailSender
{
	private Properties properties;

	public MailSender(String host, String from)
	{
		properties = new Properties();
		properties.put("mail.transport.protocol", "smtp");
		properties.put("mail.smtp.host", host);
		properties.put("mail.from", from);
	}

	public void send(String to, String subject, String body) throws MessagingException
	{
		Session session = Session.getDefaultInstance(properties);
		MimeMessage message = new MimeMessage(session);
		message.setFrom(InternetAddress.getLocalAddress(session));
		message.setRecipients(RecipientType.TO, to);
		message.setSubject(subject);
		message.setText(body);

		Transport.send(message);
	}
}