CodeceptJSで受信メールの検証、MailSlurp vs gmail　お薦めはMailSlurp

# 概要
ユーザー登録機能やパスワード再設定機能を持ったシステム、あるいはお客様へのシステム更新情報などの通知ではメール送信する場合があり、送信されたメール文言の確認や貼られたURLを辿ってWeb画面の操作を行う場合があります。自動テストを行う場合、一旦WebUIを離れての操作になるため、メール機能をサポートするAPIを利用したテストになります。
よく開発・実装段階では、ローカル環境にメールサーバ（注1）を立ち上げて利用することもあり、検証環境についても同環境を利用するケースもありますが、本件で紹介するのは、外部のメールサーバを利用した事例となります。

CodeceptJSでは、[MailSlurp](https://www.mailslurp.com/)を利用したhelperが公開されており（結局使わなかったけど）これを使った場合と、Node.jsのライブラリを利用してgmailを利用した場合とを比較してみました。
環境：
Windows10 64bit 20H2 64GByte 第10世代Core i7
Node v15.8.0

簡単に両者を比較すると以下のようですが、お薦めは「MailSlurp」です。

|メール受信|必要なインストール|受信手段|利用するメールサーバ|特記事項|
|:---|:---|:---|:---|:---|
|MailSlurp|MailSlurp-client|メールがこちらに送信された後のLastMailメソッド|MailSlurp|利用頻度によっては有料プランが必要。無料だと100通/月まで|
|gmail|inboxおよびmailParserおよびiconv|メール待受け中の着信に反応|gmail|IMAPが必須。gmailの外からのアプリケーションアクセスの許可が必須|

1. MailSlurpを利用する場合は、MailSlurp（利用にはアカウント作成が要ります）とMailSlurp-clientとを使う点、汎用メールサーバを利用する場合と違い、メール送信相手先を間違えた誤送信を減らすことにもなろうかと思います。（ランダム発生されたメールアドレスを利用します）
2. gmail等を使って既に手動テストをしている場合、メール本文の検証や記載されたリンクへの移動検証について自動テストにすることができます。ただし、MailSlurpのような閉じたメールシステムではないので、誤送信のリスクが残ります。また、IMAPを使えるような設定変更を伴いますので（gmailではデフォルトOffに設定されています）、メール誤送信・セキュリティといった2つのリスクを伴います。

参考にした資料：

* [CodeceptJS、MailSlurpでメールも簡単自動テスト](https://devlog.atlas.jp/2020/09/28/3381)
（こちらは、実際にパスワード再発行のフローをテストする事例紹介になっていました。）
* [CodeceptJSのマニュアル（英文）](https://codecept.io/email/#receiving-an-email)
* [MailSlurpのマニュアル（英文）](https://www.mailslurp.com/docs/)（JavaやPython、Ruby等の開発言語毎にドキュメントが用意されています ）
* [アカウント作成時のメール認証リンクをMailSlurp + Pythonで踏んでみた](https://qiita.com/GENZ_INC/items/78dd84b5b0f46b1ed00c)
* [IMAPでGmailを受信する(Node.js編)](https://qiita.com/oharato/items/35add79f406c384af780)
* [nodejsでメール受信処理とpuppeteerを組み合わせる](https://qiita.com/rh_taro/items/b155de7e45961688c443)
* [node.jsでGmailメール受信をトリガーしてなにか処理する](https://qiita.com/hideito2000/items/e3f37b145141230b5568)
* [CodeceptJSとPuppeteerでE2Eテスト（MailSlurp）](https://qiita.com/abe_n/items/32d5a41332d3c1bc4f88)

MailSlurpを利用した方が簡単です。頑張ればgmail（あるいは他のメールクライアントも）もできますが、
非同期・同期処理などのNode.jsに関するスキルも要求されますので、自作するのは根性要るかもしれません。
注1:
CodeceptJSでは、[MailCatcher連携](https://gist.github.com/schmkr/026732dfa1627b927ff3a08dc31ee884)や[MailHog連携](https://github.com/tsuemura/codeceptjs-mailhog-helper)で利用可能なPlugINやカスタムhelper等も公開されています。
# メール受信のなにを自動テストするか
例えばメール連携のWebUI機能には、私自身がユーザーの立場では、以下のようなものがあるかと思います。（もっとあるかもしれない）

1. 商品・サービスの利用開始にあたって、着信したメールに記載のkeyを、指定のWebサイトにて用いて、アクティベートする。
2. 利用中のサービスを継続して利用するために、更新時期が到来したらメールに記載のWebサイトで更新手続きをする。
3. 商品購入・サービス利用開始の後、利用できる期間限定のクーポンを入手するため、メール連絡に記載のアドレスに移動する。
4. ユーザー仮登録の後、着信したメールに記載のコードを使って、指定のWebサイトで本登録を実施する。
5. 参加予定のイベントの開始日時が近づいたら、リマインドメールが着信して、カレンダーで予定の確認ができる。
6. 商品購入後に、近くの店舗に着荷連絡をメールにて受信し、記載のリンクを携帯端末に表示させて、お店に商品を受取りに行く。
7. お薦めの商品・サービスについて、メール受信し、記載のリンクを訪れる。

着信したメールに記載されたキーワードとURLを参照して、URLの移動先でキーワードを使うといったテストケースが代表的なものとして挙げられると思います。
その他、受信メールの自動テストで確認しておきたいとしたら、

8. メール本文の長さ・行数で、検証に制約が発生しないか。文言の間違いが摘出できるか。
9. 本文からURLの検出と移動が、例えばURLっぽいのが複数記載されていると、どうなるのか。
10. テストコードの実装が、どれくらい楽に出来るか。

以上を鑑みて、メール受信回りの自動テストを、[Selenium学習サイト](https://hotel.testplanisphere.dev/ja/)のログイン機能を使って、下記のような概要としました。

メール本文：
> [会員ランク]
> [会員氏名]様。
> この度は当ホテルに会員登録いただき、誠にありがとうございました。
> 登録手続きが完了いたしましたので、ご連絡申し上げます。
> ユーザーID：[メールアドレス]
> パスワードは登録時に設定頂いたものをお使い頂けます。
> ご登録頂いた内容につきましては、以下のアドレスにてご確認頂けます。
> [URL]
> それでは、ご利用を心からお待ち申し上げております。
> [トップページのURL]

以上のメール本文から、ユーザーIDをログインページで入力して、ユーザー情報のページに移動した後、会員ランクと会員氏名が間違っていないか（メールに記載のテキストとユーザー情報ページに記載のテキストとが一致するか）のテストとします。
尚、後述するテストコードには、上記のメールを送信する機能も含んでいます。実際のシステムでは、
テストコードでメール送信するのではなく、例えばユーザー登録機能や会費の決済、あるいは、日時をトリガとしたシステムのメール送信機能（最新の着信でテストできるかもしれませんが、時限式に発動した場合はテストも直後に呼び出されると良いかと思います）が動作することになろうかと思います。
仮のメールを送信する手段として、MailSlurp-clientのメール送信機能とNodemailerを利用した関係上、テストコードに含まれている点、ご留意ください。
それではここから、それぞれのテスト環境・準備やテストコード、テスト結果について紹介します。

# MailSlurpを利用した自動テスト環境の例
結論から言うと、CodeceptJSのサイトで公開しているhelperだけでは、どうしても旨くできませんでした。
CodeceptJSを先にインストールしてから、emailのhelperをインストールしようとすると

```
PS C:\works\EmailTest\MailSlurp03> npm i @codeceptjs/mailslurp-helper --save-dev
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: undefined@undefined
npm ERR! Found: codeceptjs@3.1.2
npm ERR! node_modules/codeceptjs
npm ERR!   codeceptjs@"^3.1.2" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer codeceptjs@"^2.3.0" from @codeceptjs/mailslurp-helper@1.0.5
npm ERR! node_modules/@codeceptjs/mailslurp-helper
npm ERR!   dev @codeceptjs/mailslurp-helper@"*" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See C:\Users\mogua\AppData\Local\npm-cache\eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\mogua\AppData\Local\npm-cache\_logs\2021-09-20T08_51_54_779Z-debug.log
PS C:\works\EmailTest\MailSlurp03>
```

となり、インストールできません。codeceptJSのバージョンにシビアなのでしょうか？
仕方なく、先にhelperをインストールしてからcodeceptJSをインストールしましたが、今度は、データ駆動が使えませんでした。結局、helperでインストールされるコードの中を見たり、[mailSlurpのホームページ](https://www.mailslurp.com/docs/)を見たりしながら、改めてmailSlurp-clientをインストールして（以下）、Node.jsのコードをゴリゴリ記述することになりました。

```
npm install mailslurp-client
```

その他、assertContainをcodeceptjs-chai から利用したのでインストールしています。
CodeceptJSの初期化（コマンドラインでnpx codeceptjs init）については、helperにwebdriverioを利用して行います。前述のように、helperとしてMailslurp-client を使いませんでしたので、codecept.conf.js の修正は、allureレポートとCodecept-chaiのみです。

```codecept.conf.js
exports.config = {
  tests: 'email_test/*_test.js',
  output: './output',
  helpers: {
    WebDriver: {
      url: 'http://localhost',
      browser: 'chrome'
    },
    "ChaiWrapper" : {
      "require": "codeceptjs-chai"
    }

  },
  include: {
    I: './steps_file.js'
  },
  bootstrap: null,
  mocha: {},
  name: 'MailSlurp02',
  translation: 'ja-JP',
  plugins: {
    pauseOnFail: {},
    retryFailedStep: {
      enabled: true
    },
    tryTo: {
      enabled: true
    },
    screenshotOnFail: {
      enabled: true
    },
    "ChaiWrapper" : {
      "require": "codeceptjs-chai"
    },
    allure: {
      enabled: true
    }
  }
}
```
テストメールを送信するために、MailSlurp-clientのメール送信機能を使っています。
メールの送信と受信機能とをNodeのモジュールとして以下のように作成して臨みます。

```emailslurpCommon.js
const { MailSlurp } = require("mailslurp-client");
const mailslurp = new MailSlurp({ apiKey: "ココにAPI Key" });

let currentEmail = null;

module.exports.sendMail = function(rank, subject, simei, id, url){
    var address = 'ここにメールアドレス@mailslurp.com';

    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;
//    console.log(body);
    const sentEmail = mailslurp.inboxController.sendEmailAndConfirm(
    "ここにMailSlurpのID",
        {
            to: [address],
            subject: subject,
            body: body,
        },
    );
};

module.exports.receiveMail = async function(){
    var emails = await mailslurp.waitForLatestEmail('ここにMailSlurpのID', 60000, true);
    currentEmail = emails;
    return emails;
};
```
API keyは、MailSlurpにアカウント作成すると発行されますのでコピーして使います。
また、MailSlurpにinbox（メールを受信する箱みたいなもの）を作成すると、ランダムに生成されたIDとメールアドレスが発行されますので、こちらもテストコードにコピーしておきます。
Subjectや会員情報はテストデータを使い、送信先はMailSlurpで作成したinboxのアドレスを指定します。
メール受信は、waitForLastEmailで取得できます。
メール送受信が出来るようになりましたので、受信したメールから文言やURLを拾ってWebサイトに移動してテストするのは、

```emailTest_test.js
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 - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '松本さくら', 'sakura@example.com', 'pass1234']);
emailTestTable.add(['会員登録完了のご連絡006','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '林潤', 'jun@example.com', 'pa55w0rd!']);
emailTestTable.add(['会員登録完了のご連絡007','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('../emailslurpCommon');

//テスト用メール送信
    mailBox.sendMail(current.会員ランク,current.Subject,current.会員氏名,current.会員ID,current.url);
//ここまで

//メール受信検証
    let mails = await mailBox.receiveMail();
//文面の文言検証
    I.assertContain(mails.body, current.verifyWord);
    const memberid = await mails.body.match(/[A-Za-z0-9_.-]*@example.com/)[0];
    const url = await mails.body.match(/(https?|ftp)(:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+)/g)[0];
    console.log(mails.body);
//文面から取得した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');
});
```
[Selenium学習サイト](https://hotel.testplanisphere.dev/ja/)に登録済みの4名を使ったテスト（会員登録完了のご連絡001 / 会員登録完了のご連絡005 / 会員登録完了のご連絡006 / 会員登録完了のご連絡007）と、
リンク先URLが間違っているケース（会員登録完了のご連絡002）、会員種別が間違っているケース（会員登録完了のご連絡003）、メール文言の一部が間違っているケース（会員登録完了のご連絡004）をテストデータとして準備しています。
mails.body だけで、メールから本文だけ取得できます。本文さえ取得できれば、あとは正規表現で会員IDだの移動先のURLだの抜き出すだけです。

# gmailを利用した自動テスト環境の例
gmail については、helperによるサポートは無く、またMailSlurp-clientのような専用clientも有りませんので、自作となります。（注2）
inbox と mailParserにiconv、テストメールの送信にnodemailerをインストールして臨みます。
iconvのインストールには、python2.* 必須というのも、心が折れそうになりました。
codecept.conf.js は、MailSlurp同様に、codeceptjs-chaiとAllureレポートを追加するだけです。
テストメールをnodemailerで送信するmoduleは、

```ext_program.js
module.exports.runSendMail = function(address,rank,subject,simei,id,url,callback_func) {
    var nodemailer = require('nodemailer');
    var smtpConfig = {
        port: 465,
        host: 'smtp.gmail.com',
        secureConnection: true,
        auth: {
            user: 'ココにgmailのID',
            pass: 'ココにgmailのパスワード'
        }
    };
    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;

   transporter.sendMail({
       from: '送信元アドレス',
       to: 'テストに使うgmailアドレス',
       subject: subject,
       text: body
   }, function(e, res){
       console.log(e ? e.message : res.message);
       smtp.close();
   });
    return body;
}
```
メール本文作成して、Subjectと送信先・送信元を添えてnodemailerで送信するのは、MailSlurp-clientの送信機能同様なのですが、受信がたいへんでした。mail.body なんて訳にはいきません。

```gmail_imap.js
var inbox = require('inbox');
var iconv = require('iconv');

var sleep = require('sleep-async')();
const simpleParser = require('mailparser').simpleParser;

var ptn_string  = "Content-Type: text/plain; charset=";
var ptn2_string = "Content-Type: text/html; charset=";
var ptn_encode_string = "Content-Transfer-Encoding: ";
var mime_string = "MIME-Version";
var client;

module.exports.run3 = async function(username,password) {
    var res;
    client = inbox.createConnection(false, "imap.gmail.com", {
        secureConnection: true,
        auth:{
            user: username,
            pass: password
        }
    });

    client.connect();
    sleep.sleep(2000, function(){});

    let promise1 = await new Promise((resolve, reject) => {
        client.on("connect", function(){
            resolve('Successfully connected to server' + '\n');
        });
    }).then(async(onRes) => {
        let promise2 = await new Promise((resolve, reject) => {
            client.openMailbox("INBOX", function(error, info){
                sleep.sleep(2000, function(){});
                if(error){
                    throw error;
                    reject('error');
                }
                resolve('Message count in INBOX: ' + info.count);
            });
        }).catch((openRes) => openRes);
        res = onRes + promise2;
    });
    return await res;
};

module.exports.parsedMail = async function(uid) {
    var resMail;
    var messageStreame = await client.createMessageStream(uid);
    await simpleParser(messageStreame)
      .then(mail=> {
        resMail = mail.text;
      })
      .catch(err=> {
          console.log(err);
      });
    client.close();
    return resMail;
};

module.exports.receive2 =  async function(){
    console.log('Mail listen...');
    var res;
    let promise3 = await new Promise(async(resolve, reject) => {
        await client.on("new", async function(message){
            if(message == null){
                reject('error');
            }
            var body01 = '----------------------------------------------------------' + '\n';
            var body02 = '日時:' + await message.date + '\n';
            var body03 = '送信者:' + await message.from.name + '-' + await message.from.address + '\n';
            var body04 = 'タイトル:' + await message.title + '\n';
            var body = body01 + body02 + body03 + body04;
            resolve(message.UID);
        });
    }).then((uid) => {
        res = uid;
    });
    return await res;
}

module.exports.close = function(){
    client.close();
}
```
CodeceptJSはNodeで動くのですが、今までほとんどNodeを意識せずに済んだので、そもそもノン・ブロック（記述したテストコードが単純に上から順番に動作するのでは無く、処理時間が係ることについては、次の行に記述したテストコードの実行にとりかかる。待たない。）で動作すること自体が新たな発見で、結局、Node.js の参考書をお取り寄せして、Promise化だのcallBackから値を受け取るだの、学習が必要となりました。await とか、なんとなく使っていたのですが、真面目に取り組むことになりました。inboxの仕様をガッツリ見て、時にはRAWデータも眺めながらのテストコード実装となりました。inbox以外にもimapクライアントは存在するのですが、自分の環境ではbase64デコード（simpleParser）との組み合わせで唯一旨く動いています。日本語メールのデコードが文字化けしないのが、この組み合わせが唯一でした。gmail以外にもメールサーバ（IMAPが使えること必須）はありますが、inboxのような汎用imapクライアントとデコードを頑張らないと実装できません。
run3メソッドが、inboxをgmailに接続するためのもの、receive2メソッドが待受けで着信したuidを取得するためのもの、parseMailメソッドが着信したuidのメールデータから、メール本文をbase64でデコードするためのものです。
つくづくMailSlurp-clientは簡単だと思いました。メール受信と本文抽出のコード量が随分と違います。
テストデータはMailSlurpと同じものを使いましたが、gmailの場合は、テストメール送信と着信の検証をするための、それぞれのメソッドとノン・ブロックに注意したテストコードにする必要があり、MailSlurp-clientのように、最後に着信したメールを選ぶのではなく、待受けるinboxでは、難しさがあります。

```mailTest_test.js
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 - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '松本さくら', 'sakura@example.com', 'pass1234']);
emailTestTable.add(['会員登録完了のご連絡006','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '林潤', 'jun@example.com', 'pa55w0rd!']);
emailTestTable.add(['会員登録完了のご連絡007','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 gi = require('../gmail_imap.js');
    var xp = require('../ext_program.js');

//inbox待受け
    startMessage = await gi.run3("ココにgmailのID","ココにgmailのパスワード");
    I.wait(2);
    console.log(startMessage);

//テスト用メール送信
    var sendMail = xp.runSendMail("テストに使うgmailアドレス",current.会員ランク,current.Subject,current.会員氏名,current.会員ID,current.url,null);
//ここまで

//メール受信検証
    let newID = await gi.receive2();
    if(newID != null){
        mail = await gi.parsedMail(newID);
        await console.log(mail);
    }
//文面の文言検証
    I.assertContain(mail, current.verifyWord);
    const memberid = await mail.match(/[A-Za-z0-9_.-]*@example.com/)[0];
    const url = await mail.match(/(https?|ftp)(:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+)/g)[0];

//文面から取得したURLに移動
    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');
});
```
sendmailをawaitにせず、ノン・ブロックで、次のreceiveで新規の着信を受けるということをしています。
新規の着信を待っているところにメールが送られてこないと受信できないからです。ここを間違えると、
いつまでもメールが受信できなかったり、次のテストデータで受信してしまったりします。
本件ではテストメール送信のためにsendMailしていますが、実際のWebシステムでは、画面操作（CodeceptJSで記述）と最後にclickが入ることになると思います。

注2：
Gmail-clientというのもあるのですが、環境構築に当たって、有料プランが必要になりました。

# テスト結果の確認
MailSlurpを利用した場合は、こんな感じになります。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">CodeceptJSとMailSlurpでメール文面テスト <a href="https://t.co/M65KQuAFZN">pic.twitter.com/M65KQuAFZN</a></p>&mdash; kazuhiroYoshino (@XSyrIQwKY7Yuy6m) <a href="https://twitter.com/XSyrIQwKY7Yuy6m/status/1445548125511307268?ref_src=twsrc%5Etfw">October 6, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
コンソールに着信したメールが表示されて、引き続きWebサイトに移動して検証のようすですが、
エラー混入させたデータ含めて、7件のテストが、2分弱の所要時間で終わります。
MailSlurpの管理コンソールに着信したメールも確認できます。
![mailslurp010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/509d93d6-057c-373c-9495-18841f1fe751.png)

gmailを利用した場合は、こんな感じ。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">CodeceptJSとgmailでメール文面テスト <a href="https://t.co/584BygRLZD">pic.twitter.com/584BygRLZD</a></p>&mdash; kazuhiroYoshino (@XSyrIQwKY7Yuy6m) <a href="https://twitter.com/XSyrIQwKY7Yuy6m/status/1445548535240220680?ref_src=twsrc%5Etfw">October 6, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
gmailにテスト送信した文章が届いている様子が判ります。
![gmail005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/0d2eb212-ae62-5d7c-79bc-c90a19d6aaec.png)

AllureレポートにMailSlurpとgmailとで、テスト結果に差異はありません。
また、混入させたエラーも検出できました。
10行程度の本文でしたが、Node.jsのmatch関数では、特に問題なく本文からのキーワード抽出が出来ています。
本文には2つURLが併記されていますが、移動に利用するのを最初のURLと決めていましたので

```
const url = await mail.match(/(https?|ftp)(:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+)/g)[0];
```
それが間違っていた場合は、エラー検出の対象となり、

* 文面に記載のURL間違い（ログイン画面への導線なのに、トップページになっていた）
![gmail006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/9ce393e5-350e-8d3b-06cf-aea481b6830c.png)

* 会員種別の間違いパターン（プレミアム会員なのにメール文面では一般会員）
![gmail002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/4bd03f19-faba-66e8-7d32-89b0d2a131c3.png)
こういったエラーが発生するのかどうかはさておき、万が一、会員種別がメールで間違っていたら、不安になるのではないでしょうか。

* メール文言の間違い（正しくは「ご利用を心からお待ち申し上げております」のところを「ご利用を心からお待ち申し上げます」になっている。）
![mailslurp009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/c5f9600d-c7ed-f22d-1d58-8175c6dd0e3b.png)
メール文言が一字一句、仕様通りなのかについては、自動テストしなくてもDiff等の汎用ツールで検出できると思います。ただ、このような挨拶文については、業種によってはとても大切なことである場合も、多いかと思います。

# 総括
ソフト開発・実装時のメール送受信確認の場合、外部にメール送信せずに、ローカル環境にSMTPを設置して（例えば、[mailCatcher](https://mailcatcher.me/)。こちらもCodeceptJSでhelperが存在します。）メール送信を行い、メールボックスに送信されたメールが着信して、本文の確認も行うといったことが行われていると思いますが、システムテストや受入れテスト段階になると実際に外部にメール送信して、例えばgmailで、件名や本文・貼られたリンクの検証をするケースも実際にはあろうかと思います。WebUIの外になるため、なかなか自動テストで扱いにくい案件ですが、本件のように、テスト用のメールアカウントとAPIが公開されているサービスとを使えば、自動テストも可能です。
今回の事例程度のメールなら、自動テストでサポートできるメール受信テストは、あまり魅力ないかもしれません。また、メールを受信するデバイスがテストとして有効な場合などでも、本件はあまり効果を期待できないと思います。
しかし、例えば、ワークフローにメールを使うような、メール命なシステムの場合は、メール本文に記載のキーワードに間違いが無いことのテストが自動テストでサポートされると、安心して開発に臨めると考えます。
CodeceptJSは、自動テストを推進するメンバーが、WebUIの操作や検証のためのテストコードを自前で作成しなくてもバンドルされている点が強力だと思います。
受信したメールからSubjectや送信元、本文がそれぞれ抽出できれば、あとは、Nodeの関数やCodeceptJSが活躍してくれます。CodeceptJSならWebUI操作についてテストコードを積極的に実装しないで済む分、ちょっと敷居の高そうなメール受信についても、自動テストに取り込む時間がとれるのではないでしょうか。