Laravel メール(Mailable)のテストをしないテストを書いて、メールのテストをするという選択肢
はじめに
半分ふざけたタイトルですが、半分位はまじめです。
という事で、まずはシンプルなメール送信プログラムを書いて、そのメールのテストをしつつ、タイトルの件に触れていきたいと思います。
シナリオ
/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