MailHogをつかってメール送信を含むE2Eテストを実施する

こんにちは。みなさんテスト書いてますか? Uzabase FORCAS開発チームです。

今回、FORCASから送っているメールの処理を変更するにあたり、これまで出来ていなかったメールを含むE2Eテストを実施するようにしたので、紹介したいと思います。

課題

FORCASからのメール送信は、Amazon SESのSMTPインターフェイスを利用しています。

E2Eテストを実施するにあたって、費用面や検証の取り回しを考えると、Amazon SESを利用せずにテストできるようにしたいです。 これが、Amazon SES APIのようなHTTPのAPIを利用してメール送信している場合はWireMock 等でHTTP APIのモックサーバーをたてれば簡単にテストできます。

しかし、SMTPを利用しているのでそう簡単にはいきません。 SMTPでメールを受け付けることができ、なおかつメールの内容を検証できるようなものが必要となります。

MailHog

そこで、MailHogというツールを利用することにしました。

MailHogは MailHog is an email testing tool for developers と書かれている通り、Eメールのテストを行うためのツールで、ざっくりと以下のことが行えます。

  • SMTPサーバーとしてメールを受け付けることができる
  • Web UIもしくはHTTP APIでメッセージを取得できる

これにより、

  1. アプリケーションからSMTPでメール送信
  2. MailHogで受信
  3. MailHogからメールの内容を取得して検証

ということができるようになります。

サンプル構成と利用例

サンプルの構成を以下の図に示します。

DockerでMailHogを立ち上げる

プロジェクトではMailHogをDockerで起動しています。デフォルトではSMTPは1025番ポート、HTTPは8025番ポートで受け付けます。 docker-compose.ymlだと以下のようになります。

version: '3'
services:
  mail:
    image: mailhog/mailhog
    ports:
      - "1025:1025"
      - "8025:8025"

MailHogは8025番ポートでAPIとWebを公開しています。 Webでは、以下のように送信されたメールを確認できます。

mailhoge-web

サンプルアプリケーション

サンプルとして、Sprint Bootで /mail でGETリクエストを受けたらメールを送信する単純なアプリケーションを用意しました。

@Controller
class MailController {

    @Autowired
    lateinit var mailSender: MailSender

    @GetMapping("/mail")
    fun sendMail() : ResponseEntity<String> {
        val msg = SimpleMailMessage()
        msg.setFrom("info@uzabase.com")
        msg.setTo("test@example.com")
        msg.setSubject("件名だよ")
        msg.setText("本文ですよ")
        mailSender.send(msg)
        return ResponseEntity.ok().build()
    }

application.propertiesは以下の通りです。メール送信の向き先をMailHogを立ち上げたhost, portにしています。MailHogのデフォルトのusername, passwordも合わせて設定しています。

spring.mail.host=localhost
spring.mail.port=1025
spring.mail.username=user
spring.mail.password=pass

E2Eテスト

E2EテストはGaugeというテストフレームワークを利用しています。UzabaseではplaytestというGaugeを便利に利用できるライブラリを公開しているので、ぜひそちらも利用してみてください!

今回はメール送信のテストをしたいので、以下のようなSpec(仕様)を作成しました。Gaugeに関しては本ブログでいくつか記事を公開していますので、詳しくはそちらをご参照ください。

[1] と [2] はplaytestがあらかじめ用意しているステップになるので、実装は不要です。実行される内容は書かれている内容からもわかると思いますが、/mail に対してGETリクエストを送り、レスポンスのHTTPステータスを検証しています。

[3]については後述します。

# メール送信

## メールを送信できる
* URL"/mail"にGETリクエストを送る // [1]
* レスポンスステータスコードが"200"である // [2]
* "info@uzabase.com"から"test@example.com"に件名"件名だよ"、本文"本文ですよ"でメールが送信された

上記[3]に対応するコードが以下になります。

MailHogがHTTPのAPIを8025番ポートで公開しているので、API経由でデータ取得やクリアをしています。

  • [1] MailHogの v2/search に対して送信先のメールアドレス: "test@example.com" のメールを検索するリクエストを送信しています
  • [2] v2/searchのレスポンスのJSONから、JsonPath をつかって欲しい要素を取得しています
  • [3] 日本語の件名にしているため =?UTF-8?B?5Lu25ZCN44Gg44KI?= といったデータが返却されるので、MimeUtilityをつかってデコードしています
  • [4] 本文はbase64でデコードします
class MailStep {
    private val baseUrl = "http://localhost:8025/api"

    @BeforeSuite
    fun clearAll() {
        Fuel.delete("${baseUrl}/v1/messages").response()
    }

    @Step("<from>から<to>に件名<subject>、本文<body>でメールが送信された")
    fun verifyMail(from: String, to: String, subject: String, body: String) {
        // [1]
        val (request, response, result) = Fuel.get( "${baseUrl}/v2/search",
            listOf("kind" to "to", "query" to "test@example.com")
        ).responseString()
        val responseBodyJson = result.get()
        
        // [2]
        val sentMailFrom = JsonPath.read<String>(responseBodyJson, "\$.items[0].Content.Headers.From[0]")
        val sentMailTo = JsonPath.read<String>(responseBodyJson, "\$.items[0].Content.Headers.To[0]")
        // [3]
        val sentMailSubject = MimeUtility.decodeText(JsonPath.read<String>(responseBodyJson, "\$.items[0].Content.Headers.Subject[0]"))
        // [4]
        val sentMailBody = String(Base64.getDecoder().decode(JsonPath.read<String>(responseBodyJson, "\$.items[0].Content.Body")))
        assertEquals(from, sentMailFrom)
        assertEquals(to, sentMailTo)
        assertEquals(subject, sentMailSubject)
        assertEquals(body, sentMailBody)
    }
}

まとめ

サンプルコードでメール送信を含むE2Eを実施する方法を示しました。 HTTP以外のプロトコルが出てくるとE2Eテストの敷居があがる感じがありますが、MailHogのようなツールがあると慣れたHTTPのAPIで操作ができるので、テストに組み込みやすくなります。

メール送信があってE2Eのテストを自動化できないと悩んでいる方は、ぜひ参考にしてみてください。

今回はあまり触れませんでしたがGaugeに関する記事もいくつか公開していますので、Gaugeが気になった方は読んでもらえると嬉しいです!

tech.uzabase.com

Page top