Learn how to start Testing in Laravel with Simple Examples using PHPUnit and PEST

Published on by

Learn how to start Testing in Laravel with Simple Examples using PHPUnit and PEST image

When talking about automated tests or unit tests in any programming language, there are two groups of people:

  • Those who don't write automated tests and think they're a waste of time
  • Those who do write tests and then can't imagine their work without them

So, with this article, I will try to convince the former camp to look at the other side and see the benefits, and see how easy it is to start with automated testing in Laravel.

First, let's talk about the "why" and then I'll show a few very basic examples of the "how".


Why You Need Automated Tests

Automated tests are not complicated: they just run parts of your code for you and report any errors. That's the most simple way to describe them. Imagine that you're launching a new feature in your app, and then a personal robot assistant would go and manually test the new functionality for you, also testing if the new code didn't break anything from the old features.

That's the main benefit: re-testing all the features automatically. And it may seem like extra work, but if you don't tell that "robot" to do it, then you should do it manually yourself, right? Or do you just launch the new features without too much testing, hoping that the users would report the bugs? I call that method sarcastically "fingers-crossed-driven development".

With every new feature of your app, automated tests pay off more and more.

  • Feature 1: saves X minutes of testing manually
  • Feature 2: saves 2X minutes - for feature 2 and feature 1 again
  • Feature 3: saves 3X minutes...
  • etc.

You get the idea. Imagine your application in a year or two, with new developers on the team who wouldn't even know how that "Feature 1" works or how to reproduce it for testing. So, your future self will thank you massively for writing the automated tests.

Of course, if you think that your project is a very short-term one and you don't care that much about its future... Nah, I believe in your good intentions, so let me show you how it's easy to start testing.


Our First Automated Tests

To run the first automated test in Laravel, you don't need to write any code. Yup, you've read that right. Everything is already configured and prepared in the default Laravel installation, including the first real basic example.

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

laravel new project
cd project
php artisan test

This should be the result in your console:

PASS Tests\Unit\ExampleTest
that true is true
 
PASS Tests\Feature\ExampleTest
the application returns a successful response
 
Tests: 2 passed
Time: 0.10s

If we take a look at the default Laravel /tests folder, 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);
}
}

There's no need to know any syntax, to understand what is happening here: loading the homepage and checking if the HTTP status code is "200 OK".

Also notice how the method name test_the_application_returns_a_successful_response() becomes readable text when viewing the test results, just replacing the underscore symbol with a space.

tests/Unit/ExampleTest.php:

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

This looks a bit pointless, checking that true is true? We'll talk about unit tests specifically, a bit later. For now, you need to understand what is generally happening in every test.

  • Each test file in the tests/ folder is a PHP Class extending the TestCase of PHPUnit
  • Inside of each class, you may create multiple methods, usually one method for one situation to be tested
  • Inside of each method, there are three actions: preparation of the situation, then action, and then checking (asserting) if the result 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, you just run this command:

php artisan make:test HomepageTest

It would generate the file 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);
}
}

What If Tests Fail?

Let me show you what happens if the test assertions don't return the expected result.

Let's edit 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 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

As you can see, there are two statements marked as FAIL, with explanations below and arrows to the exact test line that failed the assertion. So this is how the errors are shown. Convenient, isn't it?


Simple Real-Life Example: Registration Form

Let's get more practical and look at a real-life example. Imagine that you have a form, and you need to test various cases: check if it fails if filled with invalid data, check if it succeeds with the correct input, etc.

Did you know that the official Laravel Breeze starter kit comes with the feature tests inside? So, let's take a look at a few 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, as they are both related to the registration form: one is checking if the form is loaded correctly, and another one checks if the submission works well.

We get familiar with two other methods of checking the result, two more assertions: $this->assertAuthenticated() and $response->assertRedirect(). You can check all the available assertions in the official documentation of PHPUnit and Laravel Response. Keep in mind that some general assertions happen on $this object, while others check the specific $response from the route call.

Another important thing is the use RefreshDatabase; statement, with the trait, included above the class. It is needed when your test actions may affect the database, like in this example, registration adds a new entry in the users database table. For that, you would need to create a separate testing database that would be refreshed with php artisan migrate:fresh, every time the tests are executed.

You have two options: physically create a separate database, or use an in-memory SQLite database. It is both configured in the file phpunit.xml that comes by default 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_CONNECTION and DB_DATABASE which are commented out? If you have SQLite on your server, the easiest action is to just uncomment those lines, and your tests will run on that in-memory database.

In this test, we assert that the user is authenticated successfully and is 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 Database Testing assertions and do something like this:

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

Another Real-Life Example: Login Form

Let's take a look at one more test from 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();
}
}

This is about the login form. The logic is similar to the 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.

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

Again, Laravel generates the default factory with fake data for the User model, out of 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),
];
}
}

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

So, if we run php artisan test after 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

Feature Tests VS Unit Tests VS Others

You've seen the tests/Feature and tests/Unit subfolders. What is the difference between them? The answer is a little "philosophical".

Globally, outside of Laravel/PHP ecosystem, there are different kinds of automated tests. You can find such terms as:

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

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

In simple words, feature tests try to run the actual features of your applications: get the URL, call the API, mimic the exact behavior like filling in the form. Feature tests usually execute the same or similar thing as any project user would do, manually, in real life.

Unit tests have two meanings. Generally, you may find that any automated tests are called "unit tests", and the whole process may be called "unit testing". But in the context of feature vs unit, this process is about testing a specific non-public unit of your code, in isolation. For example, you have some Laravel class with a method that calculates something, like the total price of the order with parameters. So, your unit test would assert if the correct results are returned by that method (unit of code), 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 default unit test from Laravel:

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

As you can see, there's no RefreshDatabase, and that's one of the most common definitions of a unit test: it doesn't touch the database, it works like a "black box", in isolation from the running application.

Trying to mimic 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 absolute majority of the tests are Feature tests, not Unit tests. First, you need to test if your application works, the way how real people would use it.

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

Sometimes, writing tests requires changing the code itself, and refactoring it to be more "testable": separating the units into special classes or methods.


When/How To Run Tests?

What is the actual usage of that php artisan test, when you should run that?

There are different approaches, depending on your company workflow, but typically, you need to make sure that all tests are "green" (meaning, no errors) before you push your latest code changes to the repository.

So, you work locally on your task, and when you feel like you've finished, you run tests to make sure you didn't break 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 a long time ago.

If we take it a step further, it's possible to automate a lot of things. With various CI/CD tools, you can specify your tests to be executed whenever someone pushes the changes to a specific Git branch, or before merging the code into the production branch. The easiest workflow would be to use Github Actions, I have a separate video demonstrating it.


What Should You Test?

There are various opinions on how big your so-called "test coverage" should be: should you test every operation and every possible case on every page, or just limit your work to the most important parts.

Indeed, this is where I agree with people blaming automated testing for taking more time than bringing the actual benefits. That may 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 the potential error".

In other words, you need to prioritize your testing efforts with the question "What would happen if this code fails?" If your payment system has bugs, it will directly impact the business. Then, if your roles/permissions functionality is broken, then it's a huge security issue.

I like how Matt Stauffer phrased it during one conference: "You need to test those things first, which, if they fail, would get you fired from your job". Of course, it's an exaggeration, but you get the idea: test the important things first. And then other functionality, if you have time for it.


PEST: New Popular Alternative to PHPUnit

All the examples above are based on the default Laravel testing tool: PHPUnit. But over the years other tools were appearing in the ecosystem, and one of the latest popular ones is PEST. Created by the official Laravel employee Nuno Maduro, it has the goal of simplifying the syntax, making it even quicker to write the code for tests.

Under the hood, it runs on top of PHPUnit, as an additional layer, just trying to minimize some default repeating parts of PHPUnit code.

Let's take a look at an example. Remember the default Feature test class 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 how the same test would look 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;
  • Extending TestCase;
  • Putting actions on separate lines - in PEST, you can chain them.

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

php artisan make:test HomepageTest --pest

At the time of writing this article, PEST is quite popular among Laravel developers, but it's your personal preference whether to use this extra tool and learn its syntax, in addition to a well-known PHPUnit.


So, that's all you need to know about the basics of automated testing. From here, it's your choice on what tests to create and how to run them in your projects.

For more information, refer to the official Laravel docs on testing.

PovilasKorop photo

Creator of Courses and Tutorials at Laravel Daily

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Visit Laravel Forge
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with VILT and TALL stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
Microsoft Clarity Integration for Laravel image

Microsoft Clarity Integration for Laravel

Read article
Apply Dynamic Filters to Eloquent Models with the Filterable Package image

Apply Dynamic Filters to Eloquent Models with the Filterable Package

Read article
Asserting Exceptions in Laravel Tests image

Asserting Exceptions in Laravel Tests

Read article
Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4 image

Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4

Read article
Basset is an alternative way to load CSS & JS assets image

Basset is an alternative way to load CSS & JS assets

Read article
Integrate Laravel with Stripe Connect Using This Package image

Integrate Laravel with Stripe Connect Using This Package

Read article