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.
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.
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.
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:
<?php
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->amOnPage('/');
$I->seeResponseCodeIs(200);
$I->see('Welcome to zend-expressive', '//h1');
$I->seeLink('Middleware', 'https://github.com/zendframework/zend-stratigility');
}
This test will do the following:
/
If all of these assertions pass, then the test will have succeeded.
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.
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.
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
phpunit:
docker exec -it healthmonitor_php_1 php vendor/bin/phpunit
unit:
docker exec -it healthmonitor_php_1 php vendor/bin/codecept run unit
acceptance:
docker exec -it healthmonitor_php_1 php vendor/bin/codecept run acceptance
functional:
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.
phpunit:
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
.
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"
logoutput="/dev/stdout"
checkreturn="true" />
</target>
<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"
logoutput="/dev/stdout"
checkreturn="true" />
</target>
</project>
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
.
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.
Join the discussion
comments powered by Disqus