概要
以前、投稿した記事「CodeceptJSで受信メールの検証、MailSlurp vs gmail お薦めはMailSlurp」に利用できるメールサーバを1つ追加という話。
まず簡単に、前回紹介したものも含めて比較してみます。
| メール受信 | 必要なインストール | 受信手段 | 利用するメールサーバ | 特記事項 | 
|---|---|---|---|---|
| MailTrap | 特になし | APIを参考に自作したLastMail受信メソッド | MailTrap | 1つのinboxに50通まで。その後は古いメールから削除される。無料だと500通/月まで | 
| MailSlurp | MailSlurp-client | メールがこちらに送信された後のLastMailメソッド | MailSlurp | 利用頻度によっては有料プランが必要。無料だと100通/月まで | 
| gmail | inboxおよびmailParserおよびiconv | メール待受け中の着信に反応 | gmail | IMAPが必須。gmailの外からのアプリケーションアクセスの許可が必須 | 
| 参考にした資料: | 
専用のメール・クライアントが用意されている「MailSlurp」
APIを参考に自前でゴリゴリ受信メソッドを書く「MailTrap」
一言で言うと、そんな感じです。
MailSlurpとMailTrapの有料プランを比較してみました。参考まで。
MailSlurp

MailTrap

MailTrapは9ドルからあるみたいです。あと、inboxの数に違いがありますが、メールアドレスにエイリアスを使わなければ、inboxの数 = テストに使えるメールアドレスの数、と考えると、同じくらいの料金ではMailSlurpの方が多いように見えます。
また、MailSlurp-clientでは、開発言語についてjavaScript のみのようですが、MailTrapでは、cURLといったCUI、Ruby / Python / PHP / Java / Perl / C# についてもスニペットがありますので、自作に当たっての参考になればと思います。
メール受信して何を自動テストするか
メールを受信して、本文に含まれるキーワードを使ってWebUIの検証をするという点は前回の投稿と同等ですが、メールのSubjectについて検証対象に追加しました。
Selenium学習サイトを使って、以下のメール本文
[会員ランク]
[会員氏名]様。
この度は当ホテルに会員登録いただき、誠にありがとうございました。
登録手続きが完了いたしましたので、ご連絡申し上げます。
ユーザーID:[メールアドレス]
パスワードは登録時に設定頂いたものをお使い頂けます。
ご登録頂いた内容につきましては、以下のアドレスにてご確認頂けます。
[URL]
それでは、ご利用を心からお待ち申し上げております。
[トップページのURL]
からメールアドレスと控えておいたパスワードを使って、会員情報へのログインした後、IDと会員ランクと氏名とを評価します。
今回も、テストデータとして以下を準備して臨みました。
- 登録済みの4名を使ったテスト(会員登録完了のご連絡001 / 会員登録完了のご連絡006 / 会員登録完了のご連絡007/ 会員登録完了のご連絡008)
 - リンク先URLが間違っているケース(会員登録完了のご連絡002)
 - 会員種別が間違っているケース(会員登録完了のご連絡003)
 - メール文言の一部が間違っているケース(会員登録完了のご連絡004)
 - 会員氏名が間違っているケース(会員登録完了のご連絡005)
 
メール本文さえ取得できれば、後はURLっぽいのを抽出したり、メールアドレスっぽいのを抽出したりは、JavaScriptのmatch関数で正規表現を用いて行ないます。
.waitForLatestEmail だけでメール本文を取得できる点でMailSlurpは、とても簡単です。
今回紹介するMailTrapの場合、API仕様に従ったhttpsリクエストを投げて返信をもらうといった実装は、自前で行なう必要があります。
メールに記載のURLに移動したり、ログインをしたりは、CodeceptJSを利用します。
MailTrapを利用した場合の自動テスト環境設定について
事前にMailTrapのサイトにアカウントを作成しログイン後、Api-Tokenを取得しておきます。

また、IDおよびパスワードも画面に表示されたものを使いますので控えておきます。

inboxid(受信箱のIDみたいなもの)は、APIのドキュメントページにてApi-Tokenを使って取得します。

このAPIドキュメントですが、実際にhttpsでリクエストを出して、返信を確認することが出来ますので、テストコードを実装する際、挙動を確認しながらの作業ができます。
受信するinboxのメールアドレスは、

InboxのEmail Addressタブに表示されたものを利用します。
codecept.conf.jsですが、helperにcodeceptjs-chai、plugINにAllureレポートを追加しただけで、MailTrapに関するものは、何も追加しません。
メール送受信に関するコードは、
var nodemailer = require('nodemailer');
module.exports.SendMail = async function(rank, subject, simei, id, url, callback_func){
    var address = 'ここにMailTrapの受信ボックスアドレス';
    var nodemailer = require('nodemailer');
    var smtpConfig = {
        port: 2525,
        host: 'smtp.mailtrap.io',
        auth: {
            user: 'ここにMailTrapのID',
            pass: 'ここにMailTrapのパスワード'
        }
    };
    const transporter = nodemailer.createTransport(smtpConfig);
    var body01 = '様。\r\n';
    var body02 = 'この度は当ホテルに会員登録いただき、誠にありがとうございました。\r\n';
    var body03 = '登録手続きが完了いたしましたので、ご連絡申し上げます。\r\n';
    var body04 = 'ユーザーID:';
    var body05 = 'パスワードは登録時に設定頂いたものをお使い頂けます。\r\n';
    var body06 = 'ご登録頂いた内容につきましては、以下のアドレスにてご確認頂けます。\r\n';
    var body07 = 'それでは、ご利用を心からお待ち申し上げております。\r\nhttps://hotel.testplanisphere.dev/ja/index.html';
    var body = rank + '\r\n' + simei + body01 + body02 + body03 + body04 + id + '\r\n' + body05 + body06 + url + '\r\n' + body07;
    let info = await transporter.sendMail({
        from: 'ここは送信元のアドレス',
        to: address,
        subject: subject,
        text: body
    }, function(e, res){
       console.log(e ? e.message : res.message);
       smtp.close();
    });
    return await info;
};
module.exports.lastMailId = async function(){
    var result;
    var https = require('https');
    var url = 'https://mailtrap.io/api/v1/inboxes/"ここにinboxid"/messages';
    let options = {
        headers: {
            'Api-Token': 'ここにApi-Token'
        }
    }
    var data = [];
    let promise = await new Promise(async(resolve, reject) => {
        await https.get(url, options, async function (res) {
            await res.on('data', function(chunk) {
                data.push(chunk);
            }).on('end', function() {
                var events   = Buffer.concat(data);
                var r = JSON.parse(events);
                var matchData = r.filter(function(item, index){
                    if(item.id != '0') return true;
                });
                resolve(r[0].id);
//                console.log(r[0].id);
            });
        });
    }).then((lastId) => {
        result = lastId;
    });
    return await result;
}
module.exports.receiveSubject = async function(){
    var result;
    var https = require('https');
    var url = 'https://mailtrap.io/api/v1/inboxes/"ここにinboxid"/messages';
    let options = {
        headers: {
            'Api-Token': 'ここにApi-Token'
        }
    }
    var data = [];
    let promise = await new Promise(async(resolve, reject) => {
        await https.get(url, options, async function (res) {
            await res.on('data', function(chunk) {
                data.push(chunk);
            }).on('end', function() {
                var events   = Buffer.concat(data);
                var r = JSON.parse(events);
                var matchData = r.filter(function(item, index){
                    if(item.id != '0') return true;
                });
                resolve(r[0].subject);
//                console.log(r[0].id);
            });
        });
    }).then((lastId) => {
        result = lastId;
    });
    return await result;
}
module.exports.receiveMail = async function(mailid){
    var result;
    var https = require('https');
    var url = 'https://mailtrap.io/api/v1/inboxes/"ここにinboxid"/messages/' + mailid + '/body.txt';
    let options = {
        headers: {
            'Api-Token': 'ここにApi-Token'
        }
    }
    var data = [];
    let promise = await new Promise((resolve, reject) => {
        let client = https.get(url, options, function (res) {
            res.setEncoding('utf-8');
            res.on('data', function(chunk) {
                resolve(chunk);
            });
            res.on('close', function(){
//                console.log('Connection closed');
            });
            res.on('end', function() {
//                console.log('Response data end');
            });
            res.on('aborted', function() {
                console.log('Connection aborted');
            });
        });
    }).then((body) => {
        result = body;
    });
    return await result;
};
Node.jsのhttpsモジュールでApi-Token含めてGETし、返信がJSON形式で返ってくるので、最後に着信したメールのIDを取得し、取得したメールIDの本文をUTF-8でエンコードして取り出しています。
MailSlurp-clientの .waitLastEmailは未読の最終着信を見ているようなので、ここは、もう少し(例えばメールが到着しない場合とか想定して)改良の余地があるかもしれません。
CodeceptJSで使うテストコードは、以下のようになります。
let emailTestTable = new DataTable(['Subject', 'url', 'verifyTitle', 'verifyWord', '会員ランク', '会員氏名', '会員ID', '会員Pass']);
emailTestTable.add(['会員登録完了のご連絡001','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '山田一郎', 'ichiro@example.com', 'password']);
emailTestTable.add(['会員登録完了のご連絡002','https://hotel.testplanisphere.dev/ja/index.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '山田一郎', 'ichiro@example.com', 'password']);
emailTestTable.add(['会員登録完了のご連絡003','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '山田一郎', 'ichiro@example.com', 'password']);
emailTestTable.add(['会員登録完了のご連絡004','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げます。', 'プレミアム会員', '山田一郎', 'ichiro@example.com', 'password']);
emailTestTable.add(['会員登録完了のご連絡005','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '山田一朗', 'ichiro@example.com', 'password']);
emailTestTable.add(['会員登録完了のご連絡006','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '松本さくら', 'sakura@example.com', 'pass1234']);
emailTestTable.add(['会員登録完了のご連絡007','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '林潤', 'jun@example.com', 'pa55w0rd!']);
emailTestTable.add(['会員登録完了のご連絡008','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '木村良樹', 'yoshiki@example.com', 'pass-pass']);
Feature('emailTest');
Data(emailTestTable).Scenario('eMail_Test', async ({I , current}) => {
    var mailBox = require('../emailMailTrapCommon');
//テスト用メール送信
    var sendMail = await mailBox.SendMail(current.会員ランク,current.Subject,current.会員氏名,current.会員ID,current.url,null);
    await sleep(10000);
//    I.wait(30);
//ここまで
    var lastid = await mailBox.lastMailId();
    var sentSubject = await mailBox.receiveSubject(lastid);
    var sentBody = await mailBox.receiveMail(lastid);
    console.log(sentSubject);
    console.log(sentBody);
//Subjectの検証
    I.assertContain(sentSubject, current.Subject);
//文面の文言検証
    I.assertContain(sentBody, current.verifyWord);
    const memberid = await sentBody.match(/[A-Za-z0-9_.-]*@example.com/)[0];
    const url = await sentBody.match(/(https?|ftp)(:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+)/g)[0];
//文面から取得したURLに移動
    await I.amOnPage(url);
//移動先の検証、ここではタイトル
    I.seeTitleEquals(current.verifyTitle);
//文面から取得したアカウントでログイン
    I.fillField('email', memberid);
    I.fillField('password', current.会員Pass);
    I.waitForClickable('#login-button');
    I.click('#login-button');
//文面から移動した会員ページの検証
    I.see(current.会員ID);
    I.see(current.会員氏名);
    I.see(current.会員ランク);
    I.click('#logout-form > button');
});
function sleep(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, time);
    });
}
前述した意図的にエラーを混入させたテストデータを含めて、8件のテストが実行されます。
Subjectの検証ですが、テストデータに含まれるSubjectを、そのままメール送信しているので、本件ではメール受信した後の検証でエラーにはならず、会員ページに記載の氏名と異なっていると検出されています。実際のメール送信を含むシステムテストでは、期待されるSubject(例えば予約日付が入ったり)を含むテストデータを準備してテストに臨むことになります。
メールテスト中は、I.wait()が効いていなさそうなので、別途 sleep を実装して使っています。
テスト結果の確認
メールボックスには、テスト用のメールが着信していることが確認できます。

余談ですが、今回helperとしてplayWrightを用いて、テスト中の動画撮影機能を有効化してみました。

撮影された動画は、エラーを混入したテストデータを使った場合のみ、個別にクリップとして記録されているようです。Allureのレポートに表示されるスクリーンショットと併せて、動画も記録として残るようなので(platWrightの場合だけです。また、webm形式で記録されます。)報告資料として添付する等の用途があるかと思います。
総括
前回のMailSlurpに続いてMailTrapについてもメール受信のテストで使えますので、メール受信が関係するWebUIの自動テストの事例紹介とします。
紹介した事例にあるように、メールのSubjectと本文が、間違っていないかのテストが主旨となります。仕様との差異を間違い探しするようなことです。( 一郎 と 一朗 みたいな)
単に間違い探しであれば、あまり予算や時間をかけずにできる点で、MailSlurpやMailTrapを利用して、CodeceptJSと併用するのは、良い選択だと考えます。
ぜひ、やってみてください。
