Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Martin Betz
Martin Betz

Posted on • Originally published atmartinbetz.eu on

     

Testing mails in Laravel

The case: Test that users can send mails to other users

Imagine a simple social network where users can send emails to other users. We will use Laravel and test driven development to create an app that does exactly this.

An user visits the profile of another user and when he clicks on "Message" he can send a customized email to the other user via the system. He can even give a custom subject.

I know that for such a simple use case we could just use<a href="mailto:name@domain.com"></a> but I want to show you how to use and test email in Laravel.

The setup

You just need a fresh Laravel app. At the time of writing this article this was v7.8.1, but anything after v5.6 should be 80% matching (please report issues). For a minimal setup, remove all lines starting withDB_ in your.env, addDB_CONNECTION=sqlite, create a file calleddatabase.sqlite in the/database folder and runphp artisan migrate.

The test

We will first right an automated test. This will fail and we will add code step by step to fix the problems to finally get a test that gives us confidence that the code works as intended.

Let's create the test:php artisan make:test ContactUserTest. I will show you how my initial test looks like, but please be aware that I spent around 10 minute into thinking "How should this work?". Writing the test is when you define how you wish your app should work. We are first testing the expected action (it_does_send_a_mail_from_one_user_to_another). This is called the happy path. We will later check that unexpected behavior does not happen – the sad path.

I'll explain the details after the code, but I left some problems in there so you can see how test driven development (TDD) works. Write a failing test and fix one problem after the other until you get a passing test.

<?phpnamespace Tests\Feature;use App\User;use Illuminate\Support\Facades\Mail;use Tests\TestCase;class ContactUserTest extends TestCase{    /** @test */    public function it_does_send_a_mail_from_one_user_to_another()    {        $user_from = factory(User::class)->create();        $user_to = factory(User::class)->create();        Mail::fake();        $response = $this->post(route('contactUserMail'), [            'user_to' => $user_to,            'subject' => 'My subject',            'body' => 'My body',        ]);        $response->assertSuccessful();        Mail::assertSent(ContactUserMail::class);    }}
  • I first create two users, the sender and the receiver ($user_to)
  • We will use the logged in user as the sender, so we don't need to send this info with the request
  • Mail::fake() will swap the actualMail class with a test library. No real mails will be sent and we can make assertions on what should happen
  • I decided that sending a mail should be aPOST request from a form and haveuser_to,subject andbody arguments. I will get theuser_from automatically from the currently logged in user to prevent that you can send in someone else's name
  • $response->assertSuccessful() will check whether thePOST request was successful
  • Finally, I check whether the Mail classContactUserMail was sent

As expected, there are a couple of errors, but letphpunit tell us what's wrong…

Just runphpunit within your project - with Laravel 7+ you can also usephp artisan test.

Problem 01: No database being used

The error:SQLSTATE[HY000]: General error: 1 no such table: users

Our test is not using the database yet, let's add theRefreshDatabase trait to solve this.

Note: I will show git diffs for solving the problems. This diff compares beforea/ and afterb/. A+ means that something was added, a- shows deleted lines.

diff --git a/tests/Feature/ContactUserTest.php b/tests/Feature/ContactUserTest.phpindex 89de017..a611b89 100644--- a/tests/Feature/ContactUserTest.php+++ b/tests/Feature/ContactUserTest.php@@ -3,11 +3,14 @@ namespace Tests\Feature; use App\User;+use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Mail; use Tests\TestCase; class ContactUserTest extends TestCase {+ use RefreshDatabase;+     /** @test */     public function it_does_send_a_mail_from_one_user_to_another()     {

Problem 02: Route not defined

The error:Symfony\Component\Routing\Exception\RouteNotFoundException: Route [contactUserMail] not defined.

Well, we called a route that does not exist yet. Let's create it:

diff --git a/routes/web.php b/routes/web.phpindex b130397..4ecc7b7 100644--- a/routes/web.php+++ b/routes/web.php@@ -16,3 +16,7 @@ use Illuminate\Support\Facades\Route; Route::get('/', function () {     return view('welcome'); });++Route::post('contactUserMail', function() {+ return true;+})->name('contactUserMail');

This route may seem stupid, because I just returntrue, but for now it is just important to solve the error thatphpunit showed.

Problem 03: Mail was not sent

The error:The expected [Tests\Feature\ContactUserMail] mailable was not sent.

Ok, let's create the email first and leave as is:

php art make:mail ContactUserMail -m emails.contactUserMail

This will create the mail class and a markdown template.

We have to update the route to actually send the mail withMail::to('test@test.com')->send(new ContactUserMail);. Again, the goal is not to be right, but to get the test passing.

diff --git a/routes/web.php b/routes/web.phpindex 4ecc7b7..9ed75ee 100644--- a/routes/web.php+++ b/routes/web.php@@ -1,5 +1,7 @@ <?php+use App\Mail\ContactUserMail;+use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Route; /*@@ -18,5 +20,6 @@ Route::get('/', function () { }); Route::post('contactUserMail', function() {+ Mail::to('test@test.com')->send(new ContactUserMail);     return true; })->name('contactUserMail');

Lastly, we need to import the Mail class to the test:

diff --git a/tests/Feature/ContactUserTest.php b/tests/Feature/ContactUserTest.phpindex a611b89..4d208a6 100644--- a/tests/Feature/ContactUserTest.php+++ b/tests/Feature/ContactUserTest.php@@ -2,6 +2,7 @@ namespace Tests\Feature;+use App\Mail\ContactUserMail; use App\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Mail;

Runphpunit again and…✔ It does send a mail from one user to another.

Congratulations, our test is passing, we are sending the mail as expected.

But wait, we did not check thatuser_from anduser_to are right. Our test is incomplete.

This is also a common case, so no worries. Just add more assertions to your test:

diff --git a/tests/Feature/ContactUserTest.php b/tests/Feature/ContactUserTest.phpindex fb6532d..64df550 100644--- a/tests/Feature/ContactUserTest.php+++ b/tests/Feature/ContactUserTest.php@@ -28,6 +28,10 @@ class ContactUserTest extends TestCase         $response->assertSuccessful();- Mail::assertSent(ContactUserMail::class);+ Mail::assertSent(ContactUserMail::class, function($mail) use ($user_from, $user_to) {+ $mail->build();+ $this->assertEquals($user_from->email, $mail->from[0]['address']);+ $this->assertTrue($mail->hasTo($user_to->email));+ return true;+ });     } }

Runphpunit again.

Problem 04: From is not right

Error:ErrorException: Trying to get property 'address' of non-object

Ok, we did not set the sender yet. Let's do this.

diff --git a/app/Mail/ContactUserMail.php b/app/Mail/ContactUserMail.phpindex 017b66c..0577a18 100644--- a/app/Mail/ContactUserMail.php+++ b/app/Mail/ContactUserMail.php@@ -28,6 +28,6 @@ class ContactUserMail extends Mailable      */     public function build()     {- return $this->markdown('emails.contactUserMail');+ return $this->from('sender@test.com')->markdown('emails.contactUserMail');     } }diff --git a/tests/Feature/ContactUserTest.php b/tests/Feature/ContactUserTest.phpindex d47f85f..947757c 100644--- a/tests/Feature/ContactUserTest.php+++ b/tests/Feature/ContactUserTest.php@@ -15,7 +15,9 @@ class ContactUserTest extends TestCase     /** @test */     public function it_does_send_a_mail_from_one_user_to_another()     {- $user_from = factory(User::class)->create();+ $user_from = factory(User::class)->create([+ 'email' => 'sender@test.com',+ ]);         $user_to = factory(User::class)->create();     } }

Problem 05: From is not right

Error:Failed asserting that false is true(ContactUserTest.php:36)

So, now theto is not correct. Of course, we did not specify the email in the factory, so we have differing emails. Let's fix this!

diff --git a/tests/Feature/ContactUserTest.php b/tests/Feature/ContactUserTest.phpindex 9f2b79d..3f72373 100644--- a/tests/Feature/ContactUserTest.php+++ b/tests/Feature/ContactUserTest.php@@ -18,7 +18,9 @@ class ContactUserTest extends TestCase         $user_from = factory(User::class)->create([             'email' => 'sender@test.com',         ]);- $user_to = factory(User::class)->create();+ $user_to = factory(User::class)->create([+ 'email' => 'test@test.com',+ ]);         Mail::fake();

Runphpunit again and boom,from andto work as expected!

Butsubject is still missing. Add an assertion to the test:

diff --git a/tests/Feature/ContactUserTest.php b/tests/Feature/ContactUserTest.phpindex 3f72373..9840e1b 100644--- a/tests/Feature/ContactUserTest.php+++ b/tests/Feature/ContactUserTest.php@@ -36,6 +36,7 @@ class ContactUserTest extends TestCase             $mail->build();             $this->assertEquals($user_from->email, $mail->from[0]['address']);             $this->assertTrue($mail->hasTo($user_to->email));+ $this->assertEquals('My subject', $mail->subject);             return true;         });     }

Problem 05: Subject not working

Error:TypeError: Argument 2 passed to PHPUnit\Framework\Assert::assertTrue() must be of the type string, null given (ContactUserTest.php:39)

This simply means that$mail->subject is empty.

Let's solve this:

diff --git a/app/Mail/ContactUserMail.php b/app/Mail/ContactUserMail.phpindex 0577a18..1e7258f 100644--- a/app/Mail/ContactUserMail.php+++ b/app/Mail/ContactUserMail.php@@ -28,6 +28,6 @@ class ContactUserMail extends Mailable      */     public function build()     {- return $this->from('sender@test.com')->markdown('emails.contactUserMail');+ return $this->from('sender@test.com')->subject('My subject')->markdown('emails.contactUserMail');     } }

Runphpunit again and yay, the test is passing. You can be confident now that an email is sent with the expectedfrom,to andsubject.

Let's call it a day for now and tap yourself on the shoulder, this was massive.

There will be a part 2 because, to be honest, we did not completely solve it. Right now, all the values are hard coded and not coming from a user's request. We will tackle this soon.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Laravel developer, agile organisation developer; Berlin, Germany, I coach Laravel newcomers via https://mentors.codingcoach.io/?name=Martin+Betz
  • Joined

More fromMartin Betz

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp