Docker: PHPUnit with Laravel

approximately 5 minutes of reading

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 existing database)
  • 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 fil 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 located. 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.

Sample code coverage generated by PHPUnit against the src/app.

This is it. Now go and push these bars from red to green to boost your confidence.


Words: 1152
Published in: Docker · Laravel · PHP