🕌

Laravel メール(Mailable)のテストをしないテストを書いて、メールのテストをするという選択肢

5 min read

はじめに

半分ふざけたタイトルですが、半分位はまじめです。

という事で、まずはシンプルなメール送信プログラムを書いて、そのメールのテストをしつつ、タイトルの件に触れていきたいと思います。

シナリオ

/mailsend というアドレスにGETで送信すると、最初のユーザー情報を取得し、そのユーザーにメールを送信するというプログラムを書きます。
(本来であればPOST通信な訳ですが、今回はデモという事で、GETにしています)

下記の感じになります。(web.phpに記述)

use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route;

Route::get('mailsend', function () {
    $user = User::first();

    Mail::send(new WelcomeMail($user));

    return 'done';
});

特に難しい所は無いと思います。

WelcomeMail.php は、下記の感じ。

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class WelcomeMail extends Mailable
{
    use Queueable, SerializesModels;

    public $user;

    public function __construct($user)
    {
        $this->user = $user;

        $this->from('管理者のメルアド')
            ->to($user->email)
	    ->subject('ようこそ!')
            ->text('emails.welcome');
    }

    public function build()
    {
        return $this;
    }
}

build() 内に from やらを書いてないのは、後で書きますが、テストの都合上です。
また、$user は public なプロパティの為、ビューに自動で渡ります。

emails/welcom.blade.php は、以下の感じ。

こんにちは、{{ $user->name }}さん!

ようこそ。

ではテストです。Featrueテストとします。

namespace Tests\Feature;

use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class EmailSendTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    function メールが送られるかのテスト()
    {
        $user = User::factory()->create();

        Mail::fake(); // リクエストを送る前に fake

        $this->get('/mailsend')
            ->assertOk();

        // こうとか
        Mail::assertSent(WelcomeMail::class);

        // こうとか
        Mail::assertSent(WelcomeMail::class, function ($mail) use ($user) {
            return $mail->hasFrom('管理者のメルアド') && $mail->hasTo($user->email);
        });
    }
}

基本、こんな感じでしょうか。
hasFrom や hasTo を使ってテストする場合、fakeしていると、build() メソッドが呼ばれないので、先の Mailable では、build() 以外で from やらを書きました。

で、作成するシステムにも依りますが、正直、From やら To って、固定な事が多いので、あまりテストのしがいが無いんですよね。

個人的にメールで心配になるのは、後から誰かがメール本文を修正して、誤ってメール本文を壊してしまう事なんですね。

つまり例えば、誰かが親切でメール本文を修正した際、誤って、

こんにちは、{{ $user--->name }}さん!

ようこそ。

なんてやってしまうと、完全にエラーになりますよね。
まぁ、上記のようなシンプルな本文ではそれは無いでしょうが、場合によっては、if 文などが込み入っていたりして、つい壊してしまう事もあり得ます。
そんな時、上記で書いたテストがそれを教えてくれるかと言うと、、、普通にテストにパスしてしまいます。全く役に立ちません。

では、その対策としてどんな方法があるかと言うと、私の中では2つあります。(上記のテストは、上記なりに役立ちますので、追加で書きます)

(1) メールのテストをしないテストを書く

要は、fake() やら、assertSent() とかしません。下のような感じです。

    /** @test */
    function メールをFakeしないで実際に送ろうとしてエラーにならないかテスト()
    {
        $user = User::factory()->create([
            'email' => '自分のメルアド'
        ]);

        $this->get('/mailsend')
            ->assertOk();
    }

こんな感じです。これだと実際にメールを送信しようとするので、メール本文のエラーも教えてくれます。

「おいおい、これだと実際にメールが送られてしまうじゃ無いか!」と思ってしまいますが、そうでもありません。以前の Laravel は確かにそうでしたが、今時の Laravel は、テスト中は、実際にはメールは送りません。

(若干話は逸れますが)逆に、何気に実際にメールが送られた方が便利な時もあります。
メール本文を修正して、実際どんな感じかをリアルなメーラーで確認したい時です。

通常であれば、ブラウザからフォームに入力して、確認画面を経て、送信して、とか面倒な手順が必要ですが、例えば上記に一文付け加えて、

    /** @test */
    function メールをFakeしないで実際に送ろうとしてエラーにならないかテスト()
    {
        $user = User::factory()->create([
            'email' => '自分のメルアド'
        ]);

        config(['mail.default' => 'sendmail']); // ここ

        $this->get('/mailsend')
            ->assertOk();
    }

このテストをサクッと実行すれば、リアルなメーラーで表示を確認することができます。
('sendmail'の値は、適宜、'smtp' とか自分用に置き換えて下さい)

テスト中、間違って誰か知らない人のアドレスに送らないようご注意下さい。(笑
Fakerであれば、->safeEmail() を使ってやれば安心です。(->email() ではなく)

また、この方法では(下記の(2)もそうですが)、そもそも Mail::send() を呼び出しているかどうかまではテストできませんので、先の方法とセットで書くと良いです。

話は脱線しましたが、2つ目の方法です。

(2) メール本文を確認(テスト)する

以前もやや力業で、メール本文をテストすることはできましたが、Ver.8.18 で追加されたメソッドを使うと、より直感的にテストできます。
下記のような感じです。

    /** @test */
    function メールの本文は正しいかテスト()
    {
        $user = User::factory()->create([
            'email' => '自分のアドレス',
            'name' => '与太郎',
        ]);

        $mailable = new WelcomeMail($user);
        $mailable->assertSeeInText("こんにちは、与太郎さん!");
    }

もしメール本文でエラーが起きるようであれば、当然このテストは失敗します。
この方法も個人的には好きです。

ただ、テストする際の下準備(今回であれば、User::factory()->create() する所)
が、場合によっては小面倒と言うか、微妙に (1) の方法とダブったりしまうので、
時折、自分しか管理しないテストの時は、(1) の方法と合体してしまったりもしますが、まぁそれは教科書的ではないので、おいておきましょう。

雑感

テストあると、やっぱ後々楽できます。

おかしな箇所等ありましたら、コメント下さい。

参考:日本語ドキュメント、Mail Fake
参考:日本語ドキュメント、Mailableのテスト
参考:[5.6] Set MAIL_DRIVER to array in phpunit.xml #4607

Discussion

ログインするとコメントできます