Test Suite I: Test Suite for Laravel Applications using PHPUnit and Implementing Pre-deployment Checks on Github using Github Actions
In software development, it’s a common challenge to strike a balance between speed and quality. It’s tempting to rush ahead with building and shipping new features, but this can lead to costly errors and setbacks later on if you don’t put in place essential safeguards like a robust test suite and pre-deployment checks. The end result, as shown over the years, is a never-ending cycle of bug fixing.
In this article, which is the first part of a 2-part series, I will talk about the step-by-step guide to setting up a reliable test suite and necessary pre-deployment checks for a Laravel app (one of the most popular frameworks for web development built around PHP). Then, in the 2nd part, I will discuss how to set up a test environment and enforce pre-deployment checks for a Laravel package or a PHP package at large. This guide might not necessarily be the perfect fit for all Laravel projects, but it will work perfectly well for most, and you can take out or change the test config flow based on preference and the type of app you are building. For the sake of not digressing too much, let’s get right into it.
Setting up test environments
I will be using a sample project I set up for a recent interview take-home assessment I did with Manchester Airport Group. The full code can be found on GitHub here: https://github.com/ogunsakin01/cavu-test
Setup the database
Personally, I prefer using SQLite for my testing environment for two main reasons: speed and simplicity. SQLite’s file-based nature reduces the configuration required for pre-deployment checks, streamlining the testing process. Assuming you already have you Laravel project setup completed, navigate to the database folder of your Laravel project (or any preferred location) and create a new SQLite file. In my case, I named it testing.sqlite
.
Setup your test config
In the phpunit.xml
file that can be found in the root folder of your Laravel project, uncomment these two lines.
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
and set the value of DB_DATABASE
to the location of your SQLite database file. In my case, I have this below.
<env name="DB_CONNECTION" value="sqlite"/>
<env name=”DB_DATABASE” value=”database/testing.sqlite”/>
For most developers and teams, this is enough to keep them going. However, based on the complications of some of the application I have had to setup a test suite for, I take my testing configuration a little bit further. At the beginning of the phpunit.xml file, the testing environment is bootstrapped the same way as the main app by default, as shown below:
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
But in my case, I will be setting up a different bootstrap file for testing by creating a new file in my tests folder called bootstrap.php with the content below:
<?php
use Illuminate\Contracts\Console\Kernel;
require_once __DIR__ . '/../vendor/autoload.php';
if (env('DB_CONNECTION') == 'sqlite') {
if (!file_exists(__DIR__ . '/../' . env('DB_DATABASE'))) {
file_put_contents(__DIR__ . '/../' . env('DB_DATABASE'), "");
}
}
$commands = [
'migrate:fresh',
'db:seed'
];
$app = require __DIR__ . '/../bootstrap/app.php';
$console = tap($app->make(Kernel::class))->bootstrap();
foreach ($commands as $command) {
$console->call($command);
}
use Illuminate\Contracts\Console\Kernel;: This imports the Kernel interface from Laravel’s core contracts. This interface is used to define the methods required for Laravel’s console kernel.
require_once __DIR__ . ‘/../vendor/autoload.php’;: This line includes the Composer autoloader, allowing access to all the classes and dependencies needed for the Laravel application.
Checking SQLite Database: The script checks if the configured database connection is SQLite (env(‘DB_CONNECTION’) == ‘sqlite’).
If so, it checks if the SQLite database file exists. If not (!file_exists(…)), it creates an empty SQLite database file at the specified location (file_put_contents(…)).
Defining Commands: An array $commands is defined, containing the list of Artisan commands to be executed to set up the database for testing. In this case, it contains ‘migrate:fresh’ to refresh the database migrations and ‘db:seed’ to seed the database with initial data. You can add other commands here, and being able to add more commands is one of the major benefits of setting up a different bootstrap file.
Bootstrapping Laravel: The Laravel application instance is created by requiring the bootstrap/app.php file.
The console kernel is retrieved from the application container ($app->make(Kernel::class)).
The console kernel is then bootstrapped ($console->bootstrap()).
Running Commands:Finally, each command in the $commands array is executed using the console kernel’s call method ($console->call($command)). This effectively runs the database migration and seeding commands.
Overall, this script ensures that the testing environment is properly set up with a fresh database and necessary data before running PHPUnit tests for the Laravel application.
Then point the boostrap in you phpunit file to the new boostrap file that you have just created as shown below
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
>
Update the TestCase file
One final step is to update your TestCase.php
file, which can be found in the tests folder. To do this, in the same tests folder where the TestCase
is located, create a trait that helps handle some basic setup and name it CreatesApplication
. In this trait, you should have the code shown below.
<?php
namespace Tests;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Application;
trait CreatesApplication
{
/**
* Creates the application.
*/
public function createApplication(): Application
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
}
use Illuminate\Contracts\Console\Kernel;
: This line imports the Kernel
interface from the Illuminate\Contracts\Console
namespace. The Kernel
interface is part of Laravel's core, responsible for bootstrapping the framework.
use Illuminate\Foundation\Application;
: This line imports the Application
class from the Illuminate\Foundation
namespace. The Application
class represents the Laravel application instance.
trait CreatesApplication
: This line declares a trait named CreatesApplication
. Traits are a mechanism for code reuse in single inheritance languages like PHP.
public function createApplication(): Application
: This method creates the Laravel application instance. It returns an instance of the Application
class.
$app = require __DIR__.'/../bootstrap/app.php';
: This line includes the app.php
file from the bootstrap
directory, which is the entry point for the Laravel application. It returns the $app
variable, which holds the application instance.
$app->make(Kernel::class)->bootstrap();
: This line retrieves an instance of the Kernel
class using the Laravel service container ($app->make(Kernel::class)
) and calls its bootstrap()
method. Bootstrapping initializes various components of the Laravel framework.
return $app;
: Finally, the method returns the initialized application instance.
After completing this step, return to your TestCase
class and update it to use the trait. When you are done with this, your TestCase
should look as shown below.
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
}
This completes the setup process for your unique testing environment, and you can now proceed to start writing your tests from here.
Writing Basic Tests in PHPUnit
To ensure that our test setup truly works as it should, I created an Authentication test. In my case, the solution I built requires API authentication, which needs thorough testing. However, to keep things concise, I’ll post a snippet of a simple test from the test file. You can find the full test suite in the public repo link I provided at the beginning of the article.
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
class AuthenticationTest extends TestCase
{
public array $fakeUser;
public $testUser;
public function __construct(string $name)
{
parent::__construct($name);
$this->fakeUser = [
'name' => fake()->name(),
'email' => fake()->safeEmail(),
'password' => 'password',
'password_confirmation' => 'password'
];
$this->testUser = User::create([
'name' => fake()->name(),
'email' => fake()->safeEmail(),
'password' => Hash::make('password')
]);
}
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
Then you can proceed to run your tests using either the vendor/bin/phpunit
command on Mac and vendor\bin\phpunit
command on PC, or the php artisan test
command, as shown in Fig 1a and 1b below. How you choose to run your tests is entirely up to you; either way works fine.
Setting up Github Action
Next is setting up GitHub Actions to handle pre-deployment checks. Setting this up can be very easy as GitHub already provides this out of the box. All you have to do is go to the “Actions” section of your repo on GitHub and create a new workflow. Then select the workflow of your choice. For this particular solution, I selected Laravel. A workflow file called laravel.yml
is added to a workflows
folder newly created in a newly created .github
folder. Then you can do some configurations to your workflow file based on your preferences. When I finished tweaking the laravel.yml
file, mine looked like this:
name: Laravel
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
laravel-tests:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
with:
php-version: '8.1'
- uses: actions/checkout@v3
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install Dependencies
run: composer update
- name: Generate key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Create Database
run: |
mkdir -p database
touch database/testing.sqlite
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/testing.sqlite
run: vendor/bin/phpunit
So every time you make a new push to your master branch or a pull request is made to pull into the master branch, the workflow gets triggered, which shows you if you have any breaking changes if any of your tests fail. Your workflow log will look like this
Conclusion
This covers setting up a test suite with Laravel, but there’s undoubtedly more to know about the Laravel test suite. This includes topics like parallel testing, mocking, and how to actually write the tests themselves. For more information about writing tests in Laravel, you can visit the official documentation page at https://laravel.com/docs/10.x/testing. I also found this article titled “How to Get Started with Unit Testing in Laravel” by Henry Hu very useful for writing and understanding some testing methods and techniques in Laravel.
I am currently working on more articles related to software development and data science, and I will be sharing them in the future. If you’d like to keep up with my content, follow me here on Medium, LinkedIn (https://www.linkedin.com/in/damilola-ogunsakin-560967a8/) and Twitter (@he_is_unique) to stay updated on the topics I’ll be discussing in the coming weeks and months.”