Articles

Learn how to do tests in Laravel with simple examples, using PHPUnit and PEST

When it comes to automated tests or unit tests, in any programming language, there are two opposing opinions:

  • lost time
  • You can't do without it

So, with this article we will try to convince the former, especially by demonstrating how easy it is to get started with automated testing in Laravel.

First let's talk about the "why", and then let's see some examples of the how.

Why we need automated testing

Automated tests run parts of the code and report any errors. That's the simplest way to describe them. Imagine rolling out a new feature in an app, and then a personal robot assistant would go and manually test the new feature, while also testing whether the new code didn't break any of the old features.

This is the main advantage: retesting all features automatically. This might seem like extra work, but if you don't tell the “robot” to do it, we should alternatively do it manually, right? 

Or new features could be released without testing whether they work, hoping that users will report bugs.

Automated tests can give us several advantages:

  • Save manual testing time;
  • They allow you to save time both on the new function implemented and on the consolidated functions by avoiding regression;
  • Multiply this benefit by all new features and all features already implemented;
  • The previous three points apply to every new version;
  • ...

Try to imagine your application in a year or two, with new developers on the team who don't know the code written in previous years, or even how to test it. 

Our first automated tests

To perform the first automated testing in Laravel, you don't need to write any code. Yes, you read that right. Everything is already configured and prepared in the pre-installationdefinite of Laravel, including the very first basic example.

You can try installing a Laravel project and run the first tests immediately:

laravel new project
cd project
php artisan test

This should be the result in your console:

If we take a look at the predefinite of Laravel /tests, we have two files:

tests/Feature/ExampleTest.php :

class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

You don't need to know any syntax to understand what's going on here: load the home page and check if the status code HTTP is "200 OK".

Also known as the method name test_the_application_returns_a_successful_response() becomes readable text when you view the test results, simply by replacing the underline symbol with a space.

tests/Unit/ExampleTest.php :

class ExampleTest extends TestCase
{
    public function test_that_true_is_true()
    {
        $this->assertTrue(true);
    }
}

Seems a bit pointless, checking to see if this is true? 

We'll talk specifically about unit tests a little later. For now, you need to understand what generally happens in each test.

  • Each test file in the folder /tests is a PHP class that extends the TestCase of PHPUnit
  • Within each class, you can create multiple methods, usually one method for a situation to test
  • Within each method there are three actions: preparing the situation, then taking action and then verifying (affirming) whether the outcome is as expected

Structurally, that's all you need to know, everything else depends on the exact things you want to test.

To generate an empty test class, simply run this command:

php artisan make:test HomepageTest

The file is generated tests/Feature/HomepageTest.php:

class HomepageTest extends TestCase
{
    // Replace this method with your own ones
    public function test_example()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

Now let's see what happens if a test code fails in Laravel

Let's now see what happens if the test assertions do not return the expected result.

Let's change the example tests to this:

class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/non-existing-url');
 
        $response->assertStatus(200);
    }
}
 
 
class ExampleTest extends TestCase
{
    public function test_that_true_is_false()
    {
        $this->assertTrue(false);
    }
}

And now, if we run the command php artisan test again:

 FAIL  Tests\Unit\ExampleTest
⨯ that true is true
 
 FAIL  Tests\Feature\ExampleTest
⨯ the application returns a successful response
 
---
 
• Tests\Unit\ExampleTest > that true is true
Failed asserting that false is true.
 
at tests/Unit/ExampleTest.php:16
   12▕      * @return void
   13▕      */
   14▕     public function test_that_true_is_true()
   15▕     {
➜  16▕         $this->assertTrue(false);
   17▕     }
   18▕ }
   19▕
 
• Tests\Feature\ExampleTest > the application returns a successful response
Expected response status code [200] but received 404.
Failed asserting that 200 is identical to 404.
 
at tests/Feature/ExampleTest.php:19
   15▕     public function test_the_application_returns_a_successful_response()
   16▕     {
   17▕         $response = $this->get('/non-existing-url');
   18▕
➜  19▕         $response->assertStatus(200);
   20▕     }
   21▕ }
   22▕
 
 
Tests:  2 failed
Time:   0.11s

There are two failed tests, marked as FAIL, with explanations below and arrows pointing to the exact line of tests that failed. Errors are indicated this way.

Example: Testing registration form code in Laravel

Suppose we have a form and we need to test various cases: we check if it fails with invalid data, we check if it succeeds with the correct input, etc.

The official starter kit by Laravel Breeze includes i testing the functionality within it. Let's look at some examples from there:

tests/Feature/RegistrationTest.php

use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class RegistrationTest extends TestCase
{
    use RefreshDatabase;
 
    public function test_registration_screen_can_be_rendered()
    {
        $response = $this->get('/register');
 
        $response->assertStatus(200);
    }
 
    public function test_new_users_can_register()
    {
        $response = $this->post('/register', [
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => 'password',
            'password_confirmation' => 'password',
        ]);
 
        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }
}

Here we have two tests in one class, since they are both related to the registration form: one checks if the form is loaded correctly and another checks if the submission works well.

Let us become familiar with two more methods for verifying the result, two more assertions: $this->assertAuthenticated()$response->assertRedirect(). You can check all the assertions available in the official documentation of PHPUnit e LaravelResponse . Note that some general assertions occur on the subject $this, while others check the specific $responsefrom the route call.

Another important thing is the use RefreshDatabase;statement, with the stroke, inserted above the class. It is necessary when test actions can affect the database, as in this example, logging adds a new entry in the usersdatabase table. For this, you should create a separate test database which will be updated with php artisan migrate:freshevery time the tests are run.

You have two options: physically create a separate database or use an in-memory SQLite database. Both are configured in the file phpunit.xmlprovided by defaultdefinita with Laravel. Specifically, you need this part:

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="BCRYPT_ROUNDS" value="4"/>
    <env name="CACHE_DRIVER" value="array"/>
    <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
    <!-- <env name="DB_DATABASE" value=":memory:"/> -->
    <env name="MAIL_MAILER" value="array"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="TELESCOPE_ENABLED" value="false"/>
</php>

See the DB_CONNECTIONDB_DATABASEwhich ones are commented on? If you have SQLite on your server, the simplest action is to simply uncomment those lines and your tests will run against that in-memory database.

In this test we say that the user is successfully authenticated and redirected to the correct homepage, but we can also test the actual data in the database.

In addition to this code:

$this->assertAuthenticated();
$response->assertRedirect(RouteServiceProvider::HOME);

We can also use the database test assertions and do something like this:

$this->assertDatabaseCount('users', 1);
 
// Or...
$this->assertDatabaseHas('users', [
    'email' => 'test@example.com',
]);

Example of the Login page

Let's now see another example of a Login page with Laravel Breeze

tests/Feature/AuthenticationTest.php:

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;
 
    public function test_login_screen_can_be_rendered()
    {
        $response = $this->get('/login');
 
        $response->assertStatus(200);
    }
 
    public function test_users_can_authenticate_using_the_login_screen()
    {
        $user = User::factory()->create();
 
        $response = $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);
 
        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }
 
    public function test_users_can_not_authenticate_with_invalid_password()
    {
        $user = User::factory()->create();
 
        $this->post('/login', [
            'email' => $user->email,
            'password' => 'wrong-password',
        ]);
 
        $this->assertGuest();
    }
}

It's about the login form. The logic is similar to registration, right? But three methods instead of two, so this is an example of testing both good and bad scenarios. So, the common logic is that you should test both cases: when things go well and when they fail.

Innovation newsletter
Don't miss the most important news on innovation. Sign up to receive them by email.

Also, what you see in this test is the usage of Database Factories : Laravel creates fake user ( again, on your updated test database ) and then tries to log in, with correct or incorrect credentials.

Once again, Laravel generates the factory predefinita with false data for the Usermodel, outside the box.

database/factories/UserFactory.php:

class UserFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}

You see, how many things are prepared by Laravel itself, so would it be easy for us to start testing?

So if we execute php artisan testafter installing Laravel Breeze, we should see something like this:

 PASS  Tests\Unit\ExampleTest
✓ that true is true
 
 PASS  Tests\Feature\Auth\AuthenticationTest
✓ login screen can be rendered
✓ users can authenticate using the login screen
✓ users can not authenticate with invalid password
 
 PASS  Tests\Feature\Auth\EmailVerificationTest
✓ email verification screen can be rendered
✓ email can be verified
✓ email is not verified with invalid hash
 
 PASS  Tests\Feature\Auth\PasswordConfirmationTest
✓ confirm password screen can be rendered
✓ password can be confirmed
✓ password is not confirmed with invalid password
 
 PASS  Tests\Feature\Auth\PasswordResetTest
✓ reset password link screen can be rendered
✓ reset password link can be requested
✓ reset password screen can be rendered
✓ password can be reset with valid token
 
 PASS  Tests\Feature\Auth\RegistrationTest
✓ registration screen can be rendered
✓ new users can register
 
 PASS  Tests\Feature\ExampleTest
✓ the application returns a successful response
 
Tests:  17 passed
Time:   0.61s

Functional tests compared with unit tests and others

You've seen the subfolders tests/Feature e tests/Unit ?. 

What's the difference between them? 

Globally, outside of the Laravel/PHP ecosystem, there are several types of automated testing. You can find terms like:

  • Unit tests
  • Feature testing
  • Integration tests
  • Functional tests
  • End-to-end testing
  • Acceptance tests
  • Smoke tests
  • etc.

It sounds complicated, and the actual differences between these types of tests are sometimes blurred. That's why Laravel has simplified all these confusing terms and grouped them into two: unit/feature.

Simply put, feature tests try to execute the actual functionality of your applications: get the URL, call the API, mimic the exact behavior like filling out the form. Feature tests usually perform the same or similar operations as any project user would do, manually, in real life.

Unit tests have two meanings. In general, you may find that any automated test is called “unit testing” and the whole process can be called “unit testing”. But in the context of functionality versus unit, this process is about testing a specific non-public unit of code, in isolation. For example, you have a Laravel class with a method that calculates something, like the total order price with parameters. Therefore, the unit test would state whether correct results are returned from that method (code unit), with different parameters.

To generate a unit test, you need to add a flag:

php artisan make:test OrderPriceTest --unit

The generated code is the same as the pre unit testdefiLaravel system:

class OrderPriceTest extends TestCase
{
    public function test_example()
    {
        $this->assertTrue(true);
    }
}

As you can see, it doesn't exist RefreshDatabase, and this is one of defimost common unit test definitions: it does not touch the database, it works as a “black box”, isolated from the running application.

Trying to imitate the example I mentioned earlier, let's imagine we have a service class OrderPrice.

app/Services/OrderPriceService.php:

class OrderPriceService
{
    public function calculatePrice($productId, $quantity, $tax = 0.0)
    {
        // Some kind of calculation logic
    }
}

Then, the unit test could look something like this:

class OrderPriceTest extends TestCase
{
    public function test_single_product_no_taxes()
    {
        $product = Product::factory()->create(); // generate a fake product
        $price = (new OrderPriceService())->calculatePrice($product->id, 1);
        $this->assertEquals(1, $price);
    }
 
    public function test_single_product_with_taxes()
    {
        $price = (new OrderPriceService())->calculatePrice($product->id, 1, 20);
        $this->assertEquals(1.2, $price);
    }
 
    // More cases with more parameters
}

In my personal experience with Laravel projects, the vast majority of tests are Feature tests, not Unit tests. First, you need to test whether your application works, the way real people would use it.

Next, if you have special calculations or logic you can definire as a unit, with parameters, you can create unit tests specifically for that.

Sometimes, writing tests requires modifying the code itself and refactoring it to make it more “testable”: separating the units into special classes or methods.

When/how to perform tests?

What is the actual use of this php artisan test, when should you run it?

There are different approaches, depending on your business workflow, but generally you need to ensure that all tests are “green” (i.e. error-free) before pushing the final code changes to the repository.

Then, you work locally on your task, and when you think you're done, run some tests to make sure you haven't broken anything. Remember, your code may cause bugs not only in your logic but also unintentionally break some other behavior in someone else's code written long ago.

If we take it a step further, it is possible to automate many things. With various CI/CD tools, you can specify tests to run whenever someone pushes changes to a specific Git branch or before merging code into the production branch. The simplest workflow would be to use Github Actions, I have a separate video which proves it.

What should you test?

There are different opinions on how large the so-called “test coverage” should be: try every possible operation and case on every page, or limit the work to the most important parts.

In fact, this is where I agree with people who accuse automated testing of taking more time than providing actual benefit. This can happen if you write tests for every single detail. That said, it may be required by your project: the main question is “what is the price of potential error”.

In other words, you need to prioritize your testing efforts by asking the question “What would happen if this code failed?” If your payment system has bugs, it will directly impact the business. So if the functionality of your roles/permissions is broken, this is a huge security issue.

I like how Matt Stauffer put it at a conference: “You have to first test those things that, if they fail, would get you fired from your job.” Of course that's an exaggeration, but you get the idea: try the important stuff first. And then other features, if you have time.

PEST: new alternative to PHPUnit

All the above examples are based on Laravel pre testing tooldefinite: PHPUnit . But over the years other tools have appeared in the ecosystem and one of the latest popular ones is PEST . Created by official Laravel employee Nuno Maduro , aims to simplify the syntax, making writing code for tests even faster.

Under the hood, it runs su PHPUnit, as an additional layer, just trying to minimize some pre-repeated partsdefinite of the PHPUnit code.

Let's look at an example. Remember the pre feature test classdefinited in Laravel? I will remind you:

namespace Tests\Feature;
 
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

Do you know what the same test would look like with PEST?

test('the application returns a successful response')->get('/')->assertStatus(200);

Yes, ONE line of code and that's it. So, the goal of PEST is to remove the overhead of:

  • Creating classes and methods for everything;
  • Test case extension;
  • By putting actions on separate lines: in PEST you can chain them together.

To generate a PEST test in Laravel, you need to specify an additional flag:

php artisan make:test HomepageTest --pest

As of this writing, PEST is quite popular among Laravel developers, but it's your personal preference whether to use this additional tool and learn its syntax, as well as a PHPUnit note.

BlogInnovazione.it

Innovation newsletter
Don't miss the most important news on innovation. Sign up to receive them by email.

Latest Articles

The Benefits of Coloring Pages for Children - a world of magic for all ages

Developing fine motor skills through coloring prepares children for more complex skills like writing. To color…

May 2, 2024

The Future is Here: How the Shipping Industry is Revolutionizing the Global Economy

The naval sector is a true global economic power, which has navigated towards a 150 billion market...

May 1, 2024

Publishers and OpenAI sign agreements to regulate the flow of information processed by Artificial Intelligence

Last Monday, the Financial Times announced a deal with OpenAI. FT licenses its world-class journalism…

April 30 2024

Online Payments: Here's How Streaming Services Make You Pay Forever

Millions of people pay for streaming services, paying monthly subscription fees. It is common opinion that you…

April 29 2024