[Laravel Testing 101] Writing tests for guest user functionalities on a Laravel CRUD application
This is an excerpt of my upcoming ebook Laravel Testing 101. If you haven’t read the previous chapters yet (available for free here: Adding Tests to your Laravel CRUD Application: Where to Start? and here: What should we be testing in a [laravel] CRUD application?), please do so before reading this one.
Now that we have a better idea about the functionalities we should be testing in our Laravel application, let’s start with testing what a guest can do, since it is less complicated than what a logged in user can do.
As we discussed I the previous chapter, here are the functionalities related to guests that we have in the application:
- A guest could see all the articles when visiting
/articles
- A guest could see a single article
- A guest could see a user profile
- A guest could not write a new article and get redirected to the signup page instead
- A guest could visit and get the signup page
- A guest could visit and get the login page
Make sure that PHPUnit is working properly with your application
Before we start writing any tests, let’s make sure that PHPUnit is working properly with your application.
The binary of PHPUnit is included in vendor/bin/phpunit
in your project, so all you need to do is to execute it (from your project directory).
You should see this result:
Even though we haven’t written any tests yet, Laravel includes the following example tests:
/tests/Feature/ExampleTest.php
/tests/Unit/ExampleTest.php
P.S.: I recommend adding an alias for the command above, so you would not need to type vendor/bin/phpunit
each time you want to run the tests
For example, I’m using this alias:
alias lphpunit="vendor/bin/phpunit"
1/ A guest could see all the articles when visiting /articles
Since we are going to test functionalities related to the ArticleController
, let’s first create a class dedicated to this controller.
php artisan make:test ArticleControllerTest
Note that I didn’t pass the --unit
flag to the command, which mean we are not creating a unit test but rather a feature test. The newly created class should be located in /tests/Feature/ArticleControllerTest.php
You can get rid of the testExample
that was included with the ArticleControllerTest
.
Let’s create our first tests.
When executing PHPUnit, it will look for all the public method that either start with test
or that have @test
in their dockblock.
So you could either use this format:
public function testGuestCouldSeeListOfArticles()
{
...
}
or this one:
/**
* @test
*/
public function it_allows_anyone_to_see_list_all_articles()
{
...
}
I prefer the second one since it is much easier to read.
I’ll start with the most basic test. I just want to make sure whenever I hit the /articles
route, I get a valid page back.
/**
* @test
*/
public function it_allows_anyone_to_see_list_all_articles()
{
$response = $this->get(route('get_all_articles'));
$response->assertSuccessful();
}
Save the file and run PHPUnit (either using vendor/bin/phpunit
or with the lphpunit
alias we created earlier).
Our tests passed
Note that, even though we wrote just one test and one assertion PHPUnit tells us that we have 3 tests and 3 assertions.
Note: An assertion is testing one single “thing”, a test could
contain multiple assertions. The test we wrote above contains just a
single assertion$response->assertSuccessful();
The reason behind this, is that PHPUnit will run all the tests in the /tests
directory. Running all the tests each time is not a problem at the stage we are in right now, but if you want to run just a single test, you can pass the name of the test (the name of the method) as a parameter to the --filter
flag like this:
lphpunit --filter=it_allows_anyone_to_see_list_all_articles
… and yes, as you might have guessed it you could add an alias for this command in order to save time next time you want to run just a single test.
alias lphpunit="vendor/bin/phpunit"
alias lphpunitf="lphpunit --filter="
lphpunitf it_allows_anyone_to_see_list_all_articles
As you can tell, we want to test more than just getting a valid page, we want to make sure that we are getting the right page.
We can test this with the following steps:
- make sure that we are getting the right view
- make sure that the view contains the variables needed for this page
Laravel provides two method to test the above:
$response->assertViewIs('articles.index');
$response->assertViewHas('articles');
Our test class should now look like this:
<?php
namespace Tests\Feature;use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;class ArticleControllerTest extends TestCase
{
/**
* @test
*/
public function it_allows_anyone_to_see_list_all_article()
{
$response = $this->get(route('get_all_articles')); $response->assertSuccessful();
$response->assertViewIs('articles.index');
$response->assertViewHas('articles');
}}
Now that we can test that we are getting the right view (with the right variable), we no longer need to keep the first assertion, since it is implicit.
2/ A guest could see a single article
Now that we tested that a guest user could see the list of all articles, let’s make sure that she could view individual articles as well.
In order to ensure that this functionality (showing individual articles to guest users) works as expected, we would need the following steps:
- Get an article to view (randomly)
- Generate the route to this article and send a
GET
request to it - Make sure that we are getting the right view (in this case
articles.view
) - Make sure that the view returned contains a variable named
$article
- Make sure that we are getting the article we wanted to access and not another one.
Our test should look like this:
/**
* @test
*/
public function it_allows_anyone_to_see_individual_articles()
{
$article = Article::get()->random();
$response = $this->get(route('view_article', ['id' => $article->id]));
$response->assertViewIs('articles.view');
$response->assertViewHas('article');
$returnedArticle = $response->original->article;
$this->assertEquals($article->id, $returnedArticle->id, "The returned article is different from the one we requested");
}
Note:
We can access the returned view thought the$response->original
variable
You might ask why we are doing all these steps for a such simple feature. The feature is indeed simple, and its tests are simple as well… simple, but not trivial.
We are doing all these steps to insure the following:
- We want to test accessing a random article each time (we would not want to always request the same ID or the same article), because we might have an issue in the code that makes the application always return the same article. Imagine for instance, that instead of searching for a specific article, for some reason we updated our code to use
Article::first()
, we wouldn’t be able to detect this issue if we keep returning the same article (using the same ID) over and over again. - It is highly recommended to use routes in your tests instead of URLs, because if you ever change the structure of your URL, you wouldn’t need to update your tests.
- We also want to make sure that we are indeed getting the article we are requesting, because we might get the right view that includes the variable we are looking for, but it might contain a different article from the one we requested.
3/ A guest could see a user profile
This one should look a lot like the previous test since the concept is the same (access a model and return it), but we are accessing a user instead of an article.
Since we are not testing articles here, we should create a new test class first:
php artisan make:test UserControllerTest
Then all what we need to do is to add the following test:
/**
* @test
*/
public function it_allows_anyone_to_see_users_profiles()
{
$user = User::get()->random(); $response = $this->get(route('show_user_profile', ['id' => $user->id])); $response->assertViewIs('users.show');
$response->assertViewHas('user'); $returnedUser = $response->original->user; $this->assertEquals($user->id, $returnedUser->id, "The returned user is different from the one we requested");
}
4/ A guest could not write a new article and gets redirected to the signup page instead
This one (and the remaining ones in this chapter) are much simpler, since they do not require accessing the DB.
In order to test this functionality, we need the following steps:
- attempt to access the
create_new_article
route - test whether we got redirected to the login page [login or signup page ?]
The test should look like this:
/**
* @test
*/
public function it_prevent_non_logged_in_users_from_creating_new_articles()
{
$response = $this->get(route('create_new_article'));
$response->assertRedirect('login');
}
5. A guest could visit and get the signup page and 6. A guest could visit and get the login page
These two tests are even simpler to write, since we are just checking that when we attempt to visit the login and signup page, we get valid pages. Since we are using the Laravel built-in authentication controller, we would not need to test the authentication ourselves.
We would need a new test class for these two tests as well. We could either create a dedicated class just for them. Usually I put all the “page tests” (i.e tests that ensure we are getting valid pages when we hit certain URL) in a PagesControllerTest
(especially if I have a controller named PagesController
) ; or just create a test class for HomeController
since in most cases I add the logic of the pages I tests to this class.
The two test cases should look like this:
/**
* @test
*/
public function it_returns_register_page()
{
$response = $this->get(route('register'));
$response->assertSuccessful();
}/**
* @test
*/
public function it_returns_login_page()
{
$response = $this->get(route('login'));
$response->assertSuccessful();
}
Also, instead of just checking whether we are getting a valid page (which is more than enough in this situation), we also check that we are getting the right views like this:
/**
* @test
*/
public function it_returns_register_page()
{
$response = $this->get(route('register'));
$response->assertViewIs('auth.register');
}/**
* @test
*/
public function it_returns_login_page()
{
$response = $this->get(route('login'));
$response->assertViewIs('auth.login');
}
How would tests detect breaking changes in the code base?
As we discussed in a previous chapter, one goal for writing tests is to ensure that the functionalities of the application will keep working the way we intended when we first built them.
I’d like to show just one quick example of how the tests will inform us that we are introducing a new code that is changing the behavior of the application (a breaking change).
Let’s assume that after a few weeks of working on this application we decided for some reason to update the constructor of the ArticleController
from this:
public function __construct()
{
$this->middleware("can:manage,article")->only('edit', 'update', 'delete');
$this->middleware("auth")->only('create');
}
to this:
public function __construct()
{
$this->middleware("can:manage,article")->only('edit', 'update', 'delete');
$this->middleware("auth");
}
the only change is deleting ->only('create')
from the second line of the constructor.
This might either happen by accident (a teammate didn’t see the value of protecting just one action with this middleware) or it was done intentionally to prevent guest users from reading articles before signing in.
If we run the tests, we would get this:
Before writing any tests of the application, chances are that you wouldn’t notice the breaking change before a while. Maybe the breaking change will even get deployed without anyone noticing it, since you’d be using your application as a logged in user most of the time, and you wouldn’t think that you’d need to test the guest functionalities after every small change to the application.
But with the tests, any breaking change will be detected right away without even needing to test the application manually. And if you have a CI (Continuous Integration) set up with your project (we will be exploring how to set it up later in this ebook), you wouldn’t even be able to merge/deploy without fixing the issue first.
Conclusion
In this chapter, we explored the different steps needed to test guest users functionalities. We’ve seen that even though the tests are simple, they sometimes require extra steps to ensure that we are testing the right thing, and we are not missing some edge cases (especially when we introduce a change that might break the application). We’ve also seen how tests would detect breaking changes in the code base.
In the upcoming chapters, we will be exploring tests related to logged in users which could be a little more challenging than the tests we’ve seen so far.
If you want to follow along, and get notified of any progress with this ebook (new free chapters for instance), please sign up here: https://laraveltesting101.com/