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"
/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:
Laravel provides two method to test the above:
$response->assertViewIs('articles.index');$response->assertViewHas('articles');
Our test class should now look like this:
<?phpnamespace 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.
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
request to itarticles.view
)$article
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:
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.
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");}
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:
create_new_article
routeThe 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');}
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');}
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.
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/