How to Build a Docker Test Environment

Docker makes it easy to build local development environments. But, what about being able to build test environments and run acceptance, unit, functional, and other types of tests? In this, the second part of the developing with Docker series, I’ll show you how to implement testing using PHPUnit and Codeception in the configuration which we’ve built.

How to Build a Docker Test Environment

In the first part in this series on developing web applications using Docker, we saw how to create a local development environment using Docker; one ideally suited to creating Zend Expressive (or any other kind of PHP-based web application). But, what we didn’t cover was how to handle testing in a Docker-based environment.

Don’t miss the other parts in this series

At first glance, this might not seem like all that much of a problem. However, the challenge I found, at least when I was first getting up to speed with Docker, was “how do I run the tests”?

If I was using a Vagrant-based virtual machine, then I’d run vagrant ssh and run the tests as I normally would; whether by calling phpunit or codecept. Alternatively, I could execute the tests remotely in the virtual machine, instead of ssh’ing in first.

But how do you run tests when working with Docker containers?

After a bit of searching, I found that it’s not that difficult. But you have to use the right combination of commands. At first I thought that docker run was how you ran them. Unfortunately, that booted up another instance of the container I’d specified, in a separate environment (and network), without any of the other containers being available. As a result the tests failed.

Instead, what you need to do is to use docker exec. This command connects to an existing, running, container, one which is in a network along with the other containers which we need. As a result it will be able to access the containers and execute the tests successfully.

Adding Test Support

We didn’t, explicitly, add test support in part one of this series, but most projects constructed with the Zend Skeleton installer will have some basic tests in place. If you look in test/AppTest/Action you’ll see HomePageActionTest.php.

This test class performs unit tests on the HomePageAction class. To run it, we could call vendor/bin/phpunit --configuration test/Unit/phpunit.xml. Given it’s quite elementary in what it’s assessing, running it could be done locally, without needing the containers. So, what about acceptance tests instead? These tests need all the containers in our setup.

PHPUnit doesn’t have support for acceptance tests. For that we’re going to need a tool such as Codeception, which has built-in support for acceptance tests. To make it available, run the following commands to both install it as a dependency and to create its core configuration files.

// Add it as a dependency
composer require codeception/codeception

// Create the core configuration files
vendor/bin/codecept bootstrap

With the files created, in tests/acceptance.suite.yml, set the value of url: to http://nginx. This sets the base URI to use when running acceptance tests against our application. Now we have one thing left to do, which is to actually create an acceptance test. Let’s use Codeception to create a skeleton file for us, by running the command:

vendor/bin/codecept generate:cest acceptance HomePageTest

After this, open up the file tests/acceptance/HomePageCest.php, which will look a lot like the code below:


class HomePageCest
    public function _before(AcceptanceTester $I)

    public function _after(AcceptanceTester $I)

    // tests
    public function tryToTest(AcceptanceTester $I)

With the file created, we’ll replace the three existing functions with the following one, which will perform elementary tests on the home page.

public function tryToTest(AcceptanceTester $I)
    $I->am('Guest User');
    $I->expectTo('Be able to view all journal records listed in reverse date order');
    $I->see('Welcome to zend-expressive', '//h1');
    $I->seeLink('Middleware', '');

This test will do the following:

  • Attempt to connect to the test site on the default route, /
  • Check that the response code is an HTTP 200 OK
  • Check that ‘Welcome to zend-expressive’ is set as the contents of the page’s h1 tag
  • Check that a link exists with the text of ‘Middleware’ and a href of ‘'.

If all of these assertions pass, then the test will have succeeded.

Running the Tests

Now that the test structure is in place, let’s run it. Gladly, it’s not that different from running tests in either a virtual machine, or locally. To do so, you need run the following command:

docker exec -it healthmonitor_php_1 php vendor/bin/codecept run acceptance

That’s a little long-winded. So I’ll explain what it does. Using exec Docker will execute the command php vendor/bin/codecept run acceptance in the container called healthmonitor_php_1. If you’re not familiar with the exec command, as the documentation says, it will:

Run a command in a running container

It’s quite similar to running a command on a remote server with ssh or vagrant ssh using the -c or --command switches. Don’t make the mistake of using the run command, which will run a command in a new container. This will boot up a new instance of the PHP container, on a separate network, without any of the other containers which are needed to perform the tests.

Adding Tooling Support

And that’s how to run tests for PHP-based web applications when you’re developing and running them using a Docker local development environment. But it’s reasonable to expect that you might not remember the command, or make a mistake. So let’s quickly look at a few quick ways to automate the process.

Using Make

The first suggestion I have is to use Make. This is a technique I picked up while working with an excellent group of developers at Refinery29. create a new file, called Makefile, in the root directory of your project.

In there, add the following:

all: test

.PHONY: test unit integration

test: unit functional acceptance

    docker exec -it healthmonitor_php_1 php vendor/bin/phpunit

    docker exec -it healthmonitor_php_1 php vendor/bin/codecept run unit

    docker exec -it healthmonitor_php_1 php vendor/bin/codecept run acceptance

    docker exec -it healthmonitor_php_1 php vendor/bin/codecept run functional

What we’ve done is to create a series of targets, similar to what you do in other tools, such as Phing. The first two, all and .PHONY setup the default target to run, if we don’t request one specifically. Hopefully, the final five should be fairly self-explanatory. But if not, here’s how they work, using the phpunit command as an example.

    docker exec -it healthmonitor_php_1 php vendor/bin/phpunit

The first line is the name of the target. The second line specifies the command to run when the target is called. We can also group commands together, such as in test: unit functional acceptance. Here, what we’re doing is to create a command called test which will run the unit, functional, and acceptance tests.

To run any of them, in the terminal in the root directory of your project, we call make along with the target’s name. For example, if we wanted to run the unit target, we could then call make unit. However, if we wanted to run all the tests, we could call make or make test.

Using Phing

Now what about something more recent, more PHP-specific? What about Phing? If that’s something that you’re more comfortable with, then here’s a configuration file which will provide sufficient information to get the PHPUnit and Codeception acceptance tests running.

<?xml version="1.0" encoding="UTF-8"?>

<project name="Health Monitor" default="test">

    <target name="phpunit"
            description="Run unit tests using PHPUnit in the Docker container">
        <echo msg="Running PHPUnit tests" />

        <exec command="docker exec -it healthmonitor_php_1 php vendor/bin/phpunit"
              checkreturn="true" />

    <target name="test" depends="phpunit">
        <echo msg="Running acceptance tests using Codeception" />

        <exec command="docker exec -it healthmonitor_php_1 php vendor/bin/codecept run acceptance"
              checkreturn="true" />

Here, you can see that we have a Phing XML file, called build.xml. In it, we’ve provided a project name and a default target to run, test. Then, we’ve defined two targets.

We define each target in the target XML element, where it requires a name, and can take an optional description; it’s optional, but quite handy when attempting to quickly ascertain what a target does.

Each target makes use of the echo and exec tasks. Echo prints out the string specified in msg. Exec, as you’d likely expect, runs a command, which we define in command and has the option of directing output to either stdout or to another location, as we have here by specifying /dev/stdout as the value of logoutput.

With the file created, we can run it from the command line by using the command vendor/bin/phing which will run all the targets, as test depends on phpunit. Alternatively, we can run a target by it’s name, by providing the name of the target, such as vendor/bin/phing phpunit.

Note: The coverage of Make and Phing were deliberately simplistic, as the intent was to focus on running the test commands. There will be thorough guides on Make and Phing in upcoming tutorials.

In Conclusion

And that’s how to build a test development environment using Docker. While there are many approaches to doing so, this one at least doesn’t make things overly complicated.

By making only a slight addition to your local Docker development environment, you are now able to run all your tests, regardless of their type, as easily as you would if you were using a Vagrant-based virtual machine, or one of the MAMP, WAMP, or LAMP stacks.

You might also be interested in...

Want more tutorials like this?

If so, enter your email address in the field below and click subscribe.

You can unsubscribe at any time by clicking the link in the footer of the emails you'll receive. Here's my privacy policy, if you'd like to know more. I use Mailchimp to send emails. You can learn more about their privacy practices here.

Join the discussion

comments powered by Disqus