When it comes to automated tests or unit tests, in any programming language, there are two opposing opinions:
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.
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:
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.
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.
/tests
is a PHP class that extends the TestCase of PHPUnitStructurally, 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);
}
}
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.
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()
e $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 $response
from 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 users
database table. For this, you should create a separate test database which will be updated with php artisan migrate:fresh
every 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.xml
provided 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_CONNECTION
e DB_DATABASE
which 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',
]);
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.
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 User
model, 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 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
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:
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.
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.
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.
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:
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
Developing fine motor skills through coloring prepares children for more complex skills like writing. To color…
The naval sector is a true global economic power, which has navigated towards a 150 billion market...
Last Monday, the Financial Times announced a deal with OpenAI. FT licenses its world-class journalism…
Millions of people pay for streaming services, paying monthly subscription fees. It is common opinion that you…