In the previous article about setting up a LEMP stack in Docker, I did not mention a word about essential part of crafting software and that is - testing. Let's use this stack and build on top of it, so that we can use PHPUnit to test a Laravel application both with ease as well as with confidence, that the testing environment has exact same "conditions" as the production.
What do I mean by that? By default Laravel's phpunit.xml
configuration file suggests using sqlite
as database connection and :memory:
as target database. This boosts performance and may be suitable in most cases, but how about we use MySQL for testing since we use it as a main database anyway? By keeping consistency we can mitigate a lot of potential issues that may come down the line.
At the end of this article you will have:
- Additional container named
database_test
(exact copy of the existingdatabase
) - PHPUnit up and running against new
testing
environment - code coverage being automatically generated for the entire app on each run
- all of this aliased under convenient bash script so that entire testing can be performed with a short one-liner command
Before we begin, let's clarify one thing - a single Docker container with MySQL is easily capable of having multiple databases. We could alter existing docker-compose.yml
file to achieve that, but after several attempts, none of the solutions I have found were clean and simple enough to follow. Instead I have decided to go with additional container for the testing database.
Notice ⚠️
All of the code examples you are about to see are based on the LEMP stack from the previous article. Follow it, if you haven't done so yet.
New Docker Service
Open root docker-compose.yml
and define new service which is going to be an exact copy of the database
. Name it database_test
.
# Database Test
database_test:
image: mysql:8.0.25
volumes:
- ./docker/volumes/mysql_test:/var/lib/mysql
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_USER: ${DB_USERNAME}
ports:
- "3307:3306"
The only two differences are path to the volume
on the host machine and the port
number in case we would like to access test database from the host.
Next, let's create location for container's data to persist:
mkdir -p docker/volumes/mysql_test
touch docker/volumes/mysql_test/.gitkeep
Edit root .gitignore
and add:
!docker/volumes/mysql_test
docker/volumes/mysql_test/*
!docker/volumes/mysql_test/.gitkeep
New Laravel Environment
If you look closely, you will notice that all environmental variables for MySQL point to the same variables as for non-test database. How is that possible? We simply create a separate Laravel environment.
Copy .env
to .env.testing
and ensure that this environment points DB_HOST
to newly created container (database_test
).
DB_CONNECTION=mysql
DB_HOST=database_test
DB_PORT=3306
DB_DATABASE=foo
DB_USERNAME=user
DB_PASSWORD=password
Keep in mind that this is not the entire .env.testing
file, but just an excerpt from database section. This file still has to contain essential variables like APP_ENV=testing
or APP_KEY
. 👆🏻
Now edit src/config/database.php
and copy existing mysql
array into testing
so that we define new connection that PHPUnit is going to use.
'testing' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
Installation of Xdebug
Why do I need Xdebug?
It is mandatory to be able to generate code coverage reports. These reports give a great visibility from a testing perspective. To accomplish this step correctly on our LEMP stack, open docker-compose.yml
and under the app
service add the following environment variable:
environment:
XDEBUG_MODE: coverage
Now open:
docker/services/app/app.dockerfile
and under the section where we install PHP extensions add:
RUN pecl install xdebug && docker-php-ext-enable xdebug
Now when you rebuild containers and ssh inside (./local.sh rebuild
and ./local.sh ssh
), after typing php -v
you should see:
with Xdebug v3.1.5, Copyright (c) 2002-2022, by Derick Rethans
this indicates a successful installation of Xdebug.
PHPUnit Adjustments
Let's begin with phpunit.xml
. It's located under src
directory where Laravel application is placed. Immediately after opening this file, bin the following lines:
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
Now tell PHPUnit that with each run, we want to have code coverage being generated, do this by adding:
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
after <phpunit>
and before <testsuites>
. We only want files under src/app
to be taken into consideration.
Report itself is a static HTML website with a bunch of *.html
files and nested directories. It does not have to persist but on the other hand it has to be generated somewhere.
Let's define such a place:
mkdir -p src/tests/Report
touch src/tests/Report/.gitkeep
and make sure it is not part of a git
repository by adding into root .gitignore
:
!src/tests/Report
src/tests/Report/*
!src/tests/Report/.gitkeep
New Alias for Convenience
Define new function in local.sh
:
function _test() {
find src/tests/Report -not -name .gitkeep -delete
docker-compose --env-file ./src/.env.testing exec app php artisan test --coverage-html tests/Report
}
and add new case into the switch
statement:
"test") _test ;;
to be able to call this function via ./local.sh test
.
What it does?
Every time you run this function, it is going to remove existing coverage report (leaving empty Reports
directory with .gitkeep
so that there are no git
differences).
Next, we simply run php artisan test
(a wrapper on top of ./vendor/bin/phpunit
which offers some extra output goodies). Finally we add --coverage-html
with a relative path to where the report should be generated.
Summary
We are basically done. Rebuild all containers (./local.sh rebuild
) to make sure that all changes have been applied.
Now it is assumed that you have written some feature / unit tests. You did, right? 🤡 If so, run PHPUnit (./local.sh test
). Once everything is green, the report should be generated.
To see it, open:
src/tests/Report/index.html
in any browser of your choice.
This is it. Now go and push these bars from red to green to boost your confidence.