Im Rahmen von Unit-Tests möchte man die Anbindung externer Systeme vermeiden um Stabilität der Tests zu gewährleisten. Leider hat man genau dann Probleme, wenn man gerade diese Anbindung oder die Integration mit externen Systemen testen möchte.

Für solche Unit-Test mit integrativem Charakter kann man Mocks verwenden, die recht spezifisch für den jeweiligen Anwendungsfall selbst implementiert werden. Wenn es allerdings an die Implementierung eines Mail-Server Mocks geht, ist der Aufwand der Eigenimplementierung oft nicht mehr gerechtfertigt.

Verwendung eines Fake SMTP-Servers

Mit dem Dumbster Fake SMTP Server gibt es schon eine fertige Lösung. Dumbster kann zur Laufzeit ohne umfangreiche Konfiguration gestartet werden und verhält sich wie ein echter SMTP Server:

SimpleSmtpServer.start()

Per Default lauscht der SMTP-Server auf Port 25, was auf manchen Systemen entweder nicht erlaubt ist, oder auf Systemen mit eigenem Mailserver nicht funktioniert, weil der Port bereits vergeben ist. Speziell auf Unix-Systemen ist es empfehlenswert einen Port über 1024 zu wählen, damit man keine root-Rechte benötigt. Der gewünschte Port wird einfach in der start()-Methode übergeben. Um einen freien Port zu finden, bietet sich die Implementierung einer Hilfsmethode an, die statisch aufrufbar ist und mit JDK-Mitteln einen unbenutzten Port liefert:

class PortFinder {
  public static int getAvailablePort() {
    try {
      ServerSocket socket = new ServerSocket(0);
      int unusedPort = socket.getLocalPort();
      socket.close();
      return unusedPort;
    }
    catch (IOException e) {
      throw new RuntimeException("getAvailablePort", e);
    }
  }
}

Der Start des SMTP-Servers sieht nun so aus:

int smtpPort = PortFinder.getAvailablePort();
SimpleSmtpServer simpleSmtpServer = SimpleSmtpServer.start(smtpPort);

Ein typischer Unit-Test würde nun so konfiguriert werden, dass Mails an localhost und den oben ermittelten smtpPort gesendet werden.

Einbindung in Spring Integration

Für die Anbindung eines externen Mail-Servers bietet sich Spring Integration mit seinen Mail Channel Adaptern an. Eine Konfiguration im Produktivcode könnte mit Hilfe des Namespace Supports den Mailserver wie folgt definieren:

  <si-mail:outbound-channel-adapter channel="outboundMail" mail-sender="mailSender"/>
  <bean id="mailSender">
    <property name="javaMailProperties" ref="javaMailProperties"/>
    <property name="host" value="smtp.example.com"/>
  </bean>

Wird der Test als AbstractTestNGSpringContextTests implementiert, kann die MailSender-Bean auf einfache Weise überschrieben werden. Der SimpleSmtpServer wird ebenfalls als Bean zur Verfügung gestellt. Relevant ist hier das Attribut primary="true" an der Bean mit der überschriebenen id="mailSender":

<bean id="smtpPort" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
  <property name="staticMethod" value="de.hypoport.blog.PortFinder.getAvailablePort"/>
</bean>

<bean id="simpleSmtpServer" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
  <property name="staticMethod" value="com.dumbster.smtp.SimpleSmtpServer.start"/>
  <property name="arguments" ref="smtpPort"/>
</bean>

<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl" primary="true">
  <property name="javaMailProperties" ref="javaMailProperties"/>
  <property name="host" value="localhost"/>
  <property name="port" ref="smtpPort"/>
</bean>

Der Test kann nun wie folgt implementiert werden, er bindet sowohl die produktive Spring-Konfiguration als auch die oben aufgeführte Test-Konfiguration ein:

@Test
@ContextConfiguration({"classpath:spring-config/mailsender.spring.xml",
 "classpath:spring-config/mailsender-test.spring.xml"})
public class SendMailTest extends AbstractTestNGSpringContextTests {

  @Autowired
  private SimpleSmtpServer simpleSmtpServer;
  @Resource(name = "inputChannel")
  private MessageChannel inputChannel;

  @Test
  public void testSend() throws Exception {
    String expectedSubject = "Mail Subject";
    Message<MimeMessage> mimeMessage = MessageBuilder.withPayload(createMimeMessage()).build();

    inputChannel.send(mimeMessage);
    simpleSmtpServer.stop();

    assertThat(simpleSmtpServer.getReceivedEmailSize()).isEqualTo(1);
    SmtpMessage email = (SmtpMessage) simpleSmtpServer.getReceivedEmail().next();
    assertThat(email.getHeaderValue("Subject")).isEqualTo(expectedSubject);
  }

  private MimeMessage createMimeMessage() {
    // das Erzeugen der MimeMessage ist hier nicht relevant
    return null;
  }
}

Dumbster bietet mehr als die hier gezeigten Möglichkeiten, die Menge der empfangenen Mails oder auch deren Inhalte zu verifizieren. Einige Beispiele findet man in den Beispielen der Dumbster Dokumentation.