Testing Laravel authentication flow

Image for post
Image for post

Laravel is an awesome web framework, as it provides programmers with lots of useful features like authentication, easy integration with social networking sites via Socialite and many more… Let’s stop at the authentication for a second. Many developers I know have skipped this feature of Laravel altogether, as it doesn’t suit many apps’ requirement. Tests.

If you think about it, if your app was tested (and in most cases it should be), you wouldn’t use a module that has no tests, right? Let’s fix that!

Writing your first test

Firstly, let’s talk about what php artisan make:auth offers.

It creates a few routes:

  • /login — GET and POST, to authenticate existing users
  • /logout — POST, to log out currently authenticated user
  • /register — GET and POST, to create new accounts
  • /password/email — GET and POST, to send a reset-password link
  • /password/reset/{token} — GET and POST, to reset user’s password based on the email-sent token

All of those are essential to a fully-working authentication. Let’s look at the /login route for now.

There are a few things to be tested here:

  • user can view a login form
  • user cannot view a login form when authenticated
  • user can login with correct credentials
  • user gets a correct remember-me cookie, if chooses to be remembered
  • user cannot login with a non-existent email
  • user cannot login with incorrect password
  • user can logout, when already authenticated
  • user cannot attempt to login more than five times in one minute (default behavior)

Uff. It seems like a lot. And it is going to be very hard to test, right? Actually, Laravel uses phpunit and adds tons of useful methods on top of it, so testing all of these features is extremely easy. Don’t believe me? Let’s do it together then.

First off, we will need a new file /tests/Feature/Auth/LoginTest.php . It will be a simple class that extends Laravel’s TestCase. To create that, just run php artisan make:test Auth/LoginTest. This is what you should see inside:

Of course, first, we have a namespace declaration, then some use-statements that we will come back to later, then a simple class definition, and one method.

Let’s make our first test “user can view a login form”:

Side note: Do you see how I skipped the PSR’s camel-casing? This is because I find naming my tests this way a lot more readable. You, though, name your tests however you like :)

Ok, so we have a test that does nothing. What next? Well, we want to make a request to the /login page and see if the request is successful. Also, we have to check are we served a correct view.

Now let’s run it to see if it was successful. phpunit in the console and we get a result:

Image for post
Image for post

As you can see 3 tests has been run, with 4 assertions total. That is because there are additional two example tests provided by Laravel by default. Easy, right?

Ok, now let’s see how to handle the “user cannot view a login form when authenticated”. First, we have to create a user. We could do that with old and boring:

But as you write more and more tests, this can become very tedious. Way easier and simpler, is to use Laravel’s factories.

We don’t need to persist a user in the database for this test. It’s enough that some user is authenticated. He doesn’t need to be in the DB. So a simple ->make() will be enough in this case.

What we do next, is we assign a newly created user into a variable, and use the ->actingAs($user) method on Laravel’s Test Case. Then we make a request and assert user was redirected to the homepage.

Next, we have the “user can login with correct credentials”. We create a user (this time persist in the database), set a specific password (say, ‘i-love-laravel’), make a request with user’s email and password, and assert he was logged in.

Easily said, easily done. But, if you run this test, you may see something like this:

Image for post
Image for post

That is because we haven’t set up our database yet. For running tests I suggest using sqlite and its memory database. To set it up, just add these two lines in the phpunit.xml file:

Running phpunit produces a new error:

Image for post
Image for post

And this is a very important concept in TDD and testing software. Running tests should always produce new errors. Otherwise it means you are fixing things that are not broken. Always try to fix the only thing that is noted as broken. In this case it was database connection.

Next error we have says, there is no such table users. That is because we have connected to the database, but it is empty. Let’s run migrations before each test. To do that, we need to include the RefreshDatabase trait in our test class.

Your file should now look something like this:

Run phpunit aaand…

Image for post
Image for post

We are green! Great! Let’s see how different tests may look like.

Let’s test attempting to login with incorrect password. What we have to do:

  • create a user in the database with password x
  • attempt logging in with password y
  • assert user is redirected back
  • assert there is an error in the session
  • assert email field has old input
  • assert password field does not have old input (for security measures)
  • assert user is still a guest

As you may have noticed I am using a new ->from() helper to make sure user is redirected back to the login page. Otherwise the request would come from ‘nowhere’, so user would be redirected to / .

What we have to do:

  • create a user
  • make a request to the login page with remember turned on
  • assert user is redirected to a correct page
  • assert correct cookie is attached
  • assert user is authenticated

To assert against a cookie, we have to know what is its name and what values does it hold. Name of the cookie is available through the Auth facade. Just call Auth::guard()->getRecallerName(). Cookie’s value is user-id|remember-token|user's-hashed-password . To get that, we have to construct it ourselves:

Bare in mind all cookies are encrypted by Laravel by default, so asserting against their value would be hard. Fortunately Laravel provides a helpful ->assertCookie('name', 'unencrypted value') method to assert against the value directly. So the assertion looks in the end like this:

Let’s leave logging in for now and focus on password resets module, as it sends e-mails — an interesting concept to test.

To get an email with a reset link, we have to make a request to /password/email with email we want to reset password for. Behind the scenes, Laravel leverages Notification class to send an email. So this is a facade we will be asserting against. First, we have to fake it, so that emails aren’t actually sent. Notification::fake() should do the trick. Next, we create a user and make a request.

One of the checks we have to do, is to verify a token has been saved in the database. We take a token with $token = DB::table('password_resets')->first(). Next, we assert it is not null. $this->assertNotNull($token). If token wasn’t saved, there would be no tokens in the database (as we run migrations before every test).

Next, we want to assert, that a notification has been sent to the user, the notification sent is ResetPassword class and that the token in the Notification object is the one we generated.


There are many more tests we could have written, but it would be very long and boring to read, as they become very repetitive.

But now you should know how to write feature-tests for Laravel’s authentication flow, as I have covered most important aspects of it.

Everything I have covered here, and more, is available in my auth-tests package over on GitHub. Make sure to have a look, fiddle around and make a PR if you feel like there is some necessary feature missing.

If you have any questions, or just want to say hi, do it via Twitter @CzajkowskiDarek. Or visit my website at dczajkowski.com. Make sure to see my auth-tests package, as it contains all of the tests needed for the whole flow.

Thanks y’all, and have a wonderful day! ☀️

Written by

Full-Stack Developer. 👨🏻‍💻 Computer Science student at AGH University of Science and Technology. Software engineer at airly.eu.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store