Testing PHP code using Docker

Setting up a testing environment can be a rewarding pursuit - if you do your testing before you deploy code, your tests can signal errors and stop you from deploying a critical mistake to production. Testing requires various software, depending on your development process. One of the most common php testing frameworks is phpunit. Let’s see how we can set up a Docker driven test environment.

As anything with Docker, you have the choice of many images which provide phpunit, phpcs and other development tools. While we are interested in phpunit at this point, I’ve chosen an image which provides a bunch of other tools used in PHP CI projects. The Docker image is darh/php-essentials.

When looking up the project author, I realize it’s a friend of mine also from Slovenia, Denis Arh. Well, hello there! :)

Let’s first set up a working phpunit test from an existing project, so we can see an usable approach to the unit test. You can close my MiniTPL project just to give you a reasonable test bed.

git clone https://github.com/titpetric/minitpl && cd minitpl

Now, running the unit test for the project is as simple as this:

docker run --rm -it -w /app --volume `pwd`:/app darh/php-essentials bash -c \
"set -e;
composer self-update;
composer update;
phpunit;
rm -rf composer.lock vendor"

It’s a bit of a multi-step command, but let’s break down individual bits so we understand what’s going on. I’ll keep it to the essential parts.

The Docker switch -w sets the working directory when the container runs. By default the working path is the root of the container /, which is not usable for running any kind of tests. We set the working directory to /app, and also map the current working directory (pwd) to /app with --volume.

Now, as for running tests - it’s not enough to catch just the failure of the last command, we want to break out if any of the commands fail. This is why when we start the test run, we use set -e:

      -e  Exit immediately if a command exits with a non-zero status.

So, for example, if we would put a typo in composer self-update, we would exit the test run immediately:

Warning: This development build of composer is over 30 days old. It is recommended to update it by running "/usr/local/bin/composer self-update" to get the latest version.
                                          
  [InvalidArgumentException]              
  Command "self-updates" is not defined.  
  Did you mean one of these?              
      selfupdate                          
      self-update                         

The last command exits with non zero status, which you can check like this:

# echo $?
1

For our test project we need an active composer verdor folder. Even if the project doesn’t need external dependencies, it needs this step so the unit tests can use the classes provided by the PSR-0 autoloader.

Your project may be different. If you require additional software like curl, or specific PHP extensions, you would put all the steps between set -e and the phpunit command. In case setting up your software is not trivial, is time consuming or just too complicated to do it for every testing run, consider building your own docker image and base it on darh/php-esseentials.

So, the last step of the run is phpunit. If you set up your project correctly it should run the tests defined in your phpunit.xml and give you an output a little something similar to this:

Updating to version fcce52b169799fc5973d080d31c27532ca41b7b8.
    Downloading: 100%         
Use composer self-update --rollback to return to version a309e1d89ded6919935a842faeaed8e888fbfe37
Loading composer repositories with package information
Updating dependencies (including require-dev)
Nothing to install or update
Writing lock file
Generating autoload files
PHPUnit 4.3.4 by Sebastian Bergmann.

Configuration read from /app/phpunit.xml

.........................

Time: 390 ms, Memory: 12.25Mb

OK (25 tests, 70 assertions)

Generating code coverage report in HTML format ... done

So that’s it. It’s very easy to set up an isolated testing environment with Docker, and as long as you’re dilligent about keeping your tests up to date, it provides terrific value in the long run. Consider making code coverage reports the norm, and look at which other plugins are provided in addition to phpunit. I can tell you from personal experience that having some stats about your code base can motivate programmers to get rid of some technical debt and increase code coverage. Depending on some LOC tools, putting a monetary value against lines of code can also be a nice statistic, even if I’m still not sure it has any effect on business in the real world.

“A ‘passing’ test doesn’t mean ‘no problem.’ It means no problem observed. This time. With these inputs. So far. On my machine.”

  • Michael Bolton, DevelopSense

I hope this helps anyone who wants to quickly set up a Docker based PHP testing environment.

While I have you here...

It would be great if you buy one of my books:

I promise you'll learn a lot more if you buy one. Buying a copy supports me writing more about similar topics. Say thank you and buy my books.

Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.