読者です 読者をやめる 読者になる 読者になる

文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

SubEthaMailのwiserで仮想SMTPを立ててjavaのメール送信をユニットテストする!

java ユニットテスト

メール送信はまだまだ業務で重要ですね。javaでメール送信をする処理をユニットテストしたいけどメールが大量に飛んでしまうのは困るし、誤って本番環境のメールアドレスにメールが飛んだら大変です。そんな時はwiserを使いましょう!

SubEthaMail

subethasmtp - SubEtha SMTP is an easy-to-use server-side SMTP library for Java - Google Project Hosting

wiserができる事は以下の通りです。

  • jarのみで仮想SMTPを立てられる組み込みメールサーバ機能を持つ。
  • アプリケーションからwiserに投げられたメールは破棄される。
  • wiserは送信されたメールの内容を記憶しており、メールの宛先等が取得できる。

組み込みの仮想SMTPなので例えば以下が可能になります。

  1. JUnit開始。
  2. wiserを起動して仮想SMTPを起動。
  3. ユニットテストに書かれたメール送信が実行される。
  4. wiserはlistenしているportでメールを受信するが、メールは送信しない。
  5. ユニットテストが終了する。
  6. wiserが停止し、ポートのlistenも停止。

例えばSaStrutsでメール送信されるようなActionをテストする場合、JUnit起動と同時にwiserを開始し、JUnit終了時にwiserを停止すると、テスト自体は成功するがメールは送信されません。メールは送信されませんが送信された情報(subjectやmailto等)を詳細に保持しているので、メールの宛先や件名が想定通りなのか、をテストする事が可能になります。


では今回はjavamailを使ったユニットテストを紹介します。

build.grade

テストし易いようにgradleを使います。

apply plugin: 'java'
apply plugin: 'eclipse'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

configurations {
    all*.exclude module: 'commons-logging'
    all*.exclude module: 'log4j'
    all*.exclude module: 'slf4j-jdk12'
    all*.exclude module: 'slf4j-jdk14'
}
    
dependencies {
    compile group: 'com.google.guava', name: 'guava', version: '18.0'
    compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
    compile group: 'javax.mail', name: 'mail', version: '1.4.1'
    compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.10'
    compile group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.10'
    compile group: 'org.slf4j', name: 'jul-to-slf4j', version: '1.7.10'
    compile group: 'org.slf4j', name: 'log4j-over-slf4j', version: '1.7.10'
    compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.2'
    testCompile group: 'junit', name: 'junit', version: '4.11'
    testCompile group: 'org.subethamail', name: 'subethasmtp', version: '3.1.7'
}

メール送信ユニットテスト

import java.util.Date;
import java.util.List;
import java.util.Properties;

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

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.subethamail.wiser.Wiser;
import org.subethamail.wiser.WiserMessage;

public class VirtualSmtpTest {

    public static final Logger LOGGER = LoggerFactory
            .getLogger(VirtualSmtpTest.class);
    private Wiser wiser;

    @Before
    public void before() {
        wiser = new Wiser();
        wiser.setPort(2500);
        wiser.setHostname("dummy.mail.com");
        wiser.start();
    }

    @After
    public void after() {
        wiser.stop();
    }

    @Test
    public void() throws MessagingException {
        try {
            // メール送信
            Properties prop = new Properties();
            prop.put("mail.smtp.host", "dummy.mail.com");
            prop.put("mail.smtp.port", "2500");
            Session session = Session.getDefaultInstance(prop);
            MimeMessage mime = new MimeMessage(session);
            // from
            mime.addFrom(InternetAddress.parse("from@mail.com"));
            // to
            Address[] tos = { new InternetAddress("to1@mail.com"),
                    new InternetAddress("to2@mail.com") };
            mime.setRecipients(Message.RecipientType.TO, tos);
            // cc
            Address[] ccs = { new InternetAddress("cc1@mail.com"),
                    new InternetAddress("cc2@mail.com") };
            mime.setRecipients(Message.RecipientType.CC, ccs);
            // bcc
            Address[] bcc = { new InternetAddress("bcc1@mail.com"),
                    new InternetAddress("bcc2@mail.com") };
            mime.setRecipients(Message.RecipientType.BCC, bcc);

            mime.setSubject("タイトルです", "iso-2022-jp");
            mime.setText("ほげほげ本文");
            mime.setSentDate(new Date());
            Transport.send(mime);

            // wiserが受信したデータをログ出力
            List<WiserMessage> messages = wiser.getMessages();
            for (WiserMessage wiserMessage : messages) {
                LOGGER.info("Receiver={}", wiserMessage.getEnvelopeReceiver());
                LOGGER.info("Sender={}", wiserMessage.getEnvelopeSender());
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

@beforeでwiserのインスタンス生成とwiserの開始、@afterでstop、たったそれだけです。
「dummy.mail.com」という有り得ないホスト名、port=2500という通常使わないポート番号にしておけば、誤って正しいメールサーバに向けてメールを送信しようとする自己も防ぎやすくなります。更に、メール送信処理の前にホスト名がdummy.mail.comか、portが2500か、もチェックすると、更に事故が減らせるかと思います。

これを実行した時には以下のログが出力されます。

2015/01/10 01:15:36:835 INFO - SMTPServer.start SMTP server *:2500 starting
2015/01/10 01:15:36:840 INFO - ServerThread.run SMTP server *:2500 started
2015/01/10 01:15:36:912 INFO - VirtualSmtpTest Receiver=to1@mail.com
2015/01/10 01:15:36:912 INFO - VirtualSmtpTest Sender=from@mail.com
2015/01/10 01:15:36:912 INFO - VirtualSmtpTest Receiver=to2@mail.com
2015/01/10 01:15:36:912 INFO - VirtualSmtpTest Sender=from@mail.com
2015/01/10 01:15:36:912 INFO - VirtualSmtpTest Receiver=cc1@mail.com
2015/01/10 01:15:36:913 INFO - VirtualSmtpTest Sender=from@mail.com
2015/01/10 01:15:36:913 INFO - VirtualSmtpTest Receiver=cc2@mail.com
2015/01/10 01:15:36:913 INFO - VirtualSmtpTest Sender=from@mail.com
2015/01/10 01:15:36:913 INFO - VirtualSmtpTest Receiver=bcc1@mail.com
2015/01/10 01:15:36:913 INFO - VirtualSmtpTest Sender=from@mail.com
2015/01/10 01:15:36:913 INFO - VirtualSmtpTest Receiver=bcc2@mail.com
2015/01/10 01:15:36:913 INFO - VirtualSmtpTest Sender=from@mail.com
2015/01/10 01:15:36:914 INFO - SMTPServer.stop SMTP server *:2500 stopping
2015/01/10 01:15:36:914 INFO - ServerThread.run SMTP server *:2500 stopped

WiserMessageからMimeMessageも取得できたり、rawヘッダー等も取得できたりします。

雑感

正直今までメール送信は恐怖で一杯でした。

常に「バグって本番環境のメアドに大量にメール送信したらどうしよう・・・」とか思ってたのですが、例えばpostfixやsendmail等のメールサーバーが全く入っていないEC2インスタンスを作り、そこでユニットテストすればたとえバグって本番環境のメアドでメール送信処理を実行しても安心です。

ログすら残さないので、メールアドレスという個人情報がメールサーバのログに残ってそれが漏洩してしまった!なんて事にもなりません。

ユニットテストだけでなく、ローカル環境に閉じた開発をしたい場合も、わざわざメールサーバを用意する必要が無くなるので、ちょっと送信テストしたい場合に大変有効に活用できると思います。

wiserはメール送信処理自体しないため実行速度が高速なので、ガシガシメール送信しちゃいましょう!!

Javaエンジニア養成読本[現場で役立つ最新知識、満載!]

Javaエンジニア養成読本[現場で役立つ最新知識、満載!]

  • 作者: きしだなおき,のざきひろふみ,吉田真也,菊田洋一,渡辺修司,伊賀敏樹
  • 出版社/メーカー: 技術評論社
  • 発売日: 2014/12/11
  • メディア: Kindle版
  • この商品を含むブログを見る
JavaScriptエンジニア養成読本[Webアプリ開発の定番構成Backbone.js+CoffeeScript+Gruntを1冊で習得!]

JavaScriptエンジニア養成読本[Webアプリ開発の定番構成Backbone.js+CoffeeScript+Gruntを1冊で習得!]