30 Days of Automated Testing:Using PHPUnit【D19】

Mocking (Part 4): Mail

WilliamP
4 min readFeb 9, 2023

Let’s see Mail Mocking!

Mail Mocking Functions

  • Mail::fake(): When we want to verify that an email is triggered when executing the test target behavior, but do not actually trigger the sending of the email, we can call this function in the test code.
  • Mail::assertSent(): Verifies that the specified Mailable class will be triggered to be sent. Available after calling Mail::fake().
  • Mail::assertNotSent(): Verifies that the specified Mailable class will not be triggered to be sent. Available after calling Mail::fake().
  • Mail::assertNothingSent(): Verifies that no Mailable class will be triggered to be sent. Available after calling Mail::fake().

It’s worth noting that Mail::assertSent() also has more detailed testing methods.

Mail::assertSent(MailableClass::class, function ($mail) use ($user) {
return $mail->hasTo($user->email) &&
$mail->hasCc('...') &&
$mail->hasBcc('...') &&
$mail->hasReplyTo('...') &&
$mail->hasFrom('...') &&
$mail->hasSubject('...');
});
  • $mail→hasTo: Verify the recipient.
  • $mail→hasCc: Verify the carbon copy recipient.
  • $mail→hasBcc: Verify the blind carbon copy recipient.
  • $mail→hasReplyTo: Verify if the email is a reply to the specified recipient.
  • $mail->hasFrom: Verify the sender.
  • $mail->hasSubject: Verify the subject of the email.

Let’s try it out in practice!

Example

Test Target:Forgotten password email retrieval endpoint

  • database/migrations/2014_10_12_000000_create_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('forget_password_token', 128)->nullable();
$table->rememberToken();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
};
  • app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;

/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'forget_password_token',
];

/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];

/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
  • database/factories/UserFactory.php
<?php

namespace Database\Factories;

use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition(): array
{
return [
'name' => $this->faker->name,
'email' => $this->faker->safeEmail,
'email_verified_at' => $this->faker->dateTime(),
'password' => bcrypt($this->faker->password),
'remember_token' => Str::random(10),
'forget_password_token' => Str::random(128),
];
}
}
  • app/Mail/ForgetPasswordMail.php
<?php

namespace App\Mail;

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

class ForgetPasswordMail extends Mailable
{
use Queueable, SerializesModels;

private $user;

/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}

/**
* Build the message.
*
* @return $this
*/
public function build()
{
$resetLink = config('app.url')
. "/password-reset?forget_password_token={$this->user->forget_password_token}";

return $this
->subject('To reset password')
->from('example@example.com', 'Example')
->with(['reset_link' => $resetLink])
->view('mail.forget-password-mail');
}
}
  • resources/views/mail/forget-password-mail.blade.php
<!DOCTYPE html>
<html >
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Password Reset Mail</title>
</head>
<body class="antialiased">
<a href="{{ $reset_link }}">Reset Password</a>
</body>
</html>
  • routes\api.php
<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route;
use App\Mail\ForgetPasswordMail;

Route::post('/forget-password-mail', function (Request $request) {
$email = $request->input('email');

$user = User::where('email', '=', $email)->first();

if (empty($user)) {
return response()->json([], 404);
}

$user->forget_password_token = Str::random(128);
$user->save();

Mail::to($email)
->cc('cc@test.test')
->bcc('bcc@test.test')
->send(new ForgetPasswordMail($user));

return response('', 200);
})->name('retrieve-forget-password-mail');

Test Code:

<?php

namespace Tests\Feature;

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

class MailTest extends TestCase
{
use RefreshDatabase;

public function testRetriveForgetPasswordMailSuccess()
{
$user = User::factory()->create();

$data = [
'email' => $user->email,
];

Mail::fake();

$response = $this->post(route('retrieve-forget-password-mail'), $data);
$response->assertOk();

Mail::assertSent(ForgetPasswordMail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
}

public function testRetriveForgetPasswordMailFailed()
{
$user = User::factory()->create();

$data = [
'email' => $user->email . 'x',
];

Mail::fake();

$response = $this->post(route('retrieve-forget-password-mail'), $data);
$response->assertNotFound();

Mail::assertNotSent(ForgetPasswordMail::class);
}
}

The above test code tests two test cases:

  • testRetriveForgetPasswordMailSuccess(): In this test case function, we verify that when the forget password mail retrieval endpoint is requested, if the user account actually exists, the forget password mail ForgotPasswordMail will be triggered to be sent out.
  • testRetriveForgetPasswordMailFailed(): In this test case function, we verify that when the forget password mail retrieval endpoint is requested, if the user account does not exist, the forget password mail ForgotPasswordMail will not be triggered to be sent out.

That concludes today’s introduction to Event Mocking, practice makes perfect.

Next up, let’s take a look at Queue Mocking.

If you liked this article or found it helpful, feel free to give it some claps and follow the author!

Reference

Articles of This Series

--

--