こんにちは。みなさんテスト書いてますか? 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でメッセージを取得できる
これにより、
- アプリケーションからSMTPでメール送信
- MailHogで受信
- 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では、以下のように送信されたメールを確認できます。
サンプルアプリケーション
サンプルとして、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をつかってデコードしています- 詳しくは 日本語メールの仕組み | SendGridブログ やRFCを参照してください
- [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が気になった方は読んでもらえると嬉しいです!