Testing WordPress Plugins With Docker

Do you have any experience with WordPress? Until recently, I never tried to develop something in the WordPress ecosystem. A few weeks ago, I found a cool plugin that I wanted to improve. While working on it, I realized how complex it is to write tests for WordPress — and I wanted to share my experience with you. Part of the complexity was running those tests in Docker containers — to make it easy to run them with a real WordPress environment. Because it was a bit complicated, I’ve created a template that you can use — so you could focus more on writing your tests, and less on how to run them. The template (which is actually also a sample project) is available on GitHub.

Using the Template

To use this template to test your plugin, you first need to modify a few things so it will test your actual plugin instead of the demo plugin:

Clone the repo.

Copy all the source code of your plugin to the src folder.

folder. Modify test/bootstrap.php : change to require your plugin instead of my plugin (Look for the TODO at line 21).

: change to require your plugin instead of my plugin (Look for the at line 21). Start writing tests for your plugin! You can either modify the basic tests under tests/tests-plugin-security-scanner.php or delete this file and start from scratch. The whole wordpress test SDK is available for you.

To run the tests, you first need to build the containers using:

docker-compose up --build

And than we can run the tests using:

docker-compose run wordpress vendor/bin/phpunit

Then, just sit back and watch the tests as they run.

Cool, right?

Now go ahead and start testing your plugin, it is really easy and improves the quality of your code!

So what exactly is going on behind the scenes?

Continue reading to get into the bits and bytes of how the template works.

Plugin Security Scanner

Before we dive into the details, let’s start with a little story — how I ended up developing a WordPress plugin. As you probably know, WordPress is a great platform for building almost everything- blogs, shopping sites etc. One of the things that makes WordPress so powerful is its ecosystem — WordPress has more than 50,000 plugins and thousands of themes that you can use to customize your site. Most of your specific WordPress needs have already been packed into a plugin or a theme, which is awesome! But not everything is perfect. Have you ever wondered how secure that plugin is that you’re using? After all, the plugin code is running on your site, so a vulnerability in a plugin (or, even worse — an intentionally malicious plugin) could harm your site. Luckily for us, we have WP vulnerability DB that catalogs vulnerabilities in WordPress, along with its plugins and themes. This is great — but doing it manually each time you install a new plugin or theme is not feasible. Also, it will not alert you when a new vulnerability is found in a plugin you are using.

Luckily for us, again, there is also a plugin for that — it will query WP vulnerability DB once a day with all your plugins, themes and WordPress version for vulnerabilities. If a vulnerability is found, it’ll report it to you via mail. This is great, but ideally, I’d want it to integrate with a monitoring system (we are using icinga, but any monitoring system should work). It is possible to achieve that integration with email, but webhook support will make it a lot easier.

One of the reasons I am a fan of open source is that if there is a missing feature, you can contribute to the project and add it. As this is an open source plugin, I’ve done just that. And as I was typing code, I asked myself — what about testing it? This is a pretty important plugin — and I want to be confident that it does what it’s supposed to do, and that it will keep doing it in the future.

Current WordPress Testing Tools

If you have an experience with WordPress development, you’ve probably asked yourself the same question — how to write tests for WordPress plugins (or themes, this is usually the same)?.

The good part is that WordPress can take care of that too — WordPress CLI can generate everything you need to start working on your tests. Although it’s called a unit test, integration tests seem like a more suitable name for them — as they run against real WordPress. For unit tests scenarios, it is better to use WordPress mock.

Because those are integration tests, they require an environment with PHP and MySQL installed — which makes sense — but is not ideal: (1) I don’t want to install PHP and MySQL on my machine (and on any other developer’s laptop) only to run those tests. (2) I want to have the local environment as close as possible to the environment in the CI server. Even more importantly, it needs to be close to the environment on the production server as well.(3) I wanted to avoid version hell — I don’t want to worry about installing the correct version and such.

I solved this by running the tests in Docker, although other solutions (like Vagrant) could work too. It was pretty complicated to do that — and that’s why I created a template that anyone can use for their tests (like I said in the beginning).

As I mentioned before, you can use WordPress CLI to generate all that is required to run the tests. The template is based mostly on what’s generated by WordPress, with changes made so I could run it in Docker.

Let’s take a deep dive into it so you could understand what’s going on.

Template File Structure

Before looking at the code, let’s discuss the Docker solution. The template is based on 2 containers: One will run MySQL DB (using the official docker image), and the other will run our tests (we will look into the Dockerfile soon). This could be easily done by using Docker Compose, another tool from Docker that allows you to easily define and manage multiple containers. The containers are defined using the docker-compose.yml file, which I will not discuss here – but you are more than invited to take a look!

The most interesting folder is the tests folder. Under this folder you will find 2 files:

bootstrap.php : As the name implies, this file is taking care of bootstrapping – loading WordPress SDK so it will be available for your code and your tests code, and loading your plugin code so you could test it.

: As the name implies, this file is taking care of bootstrapping – loading WordPress SDK so it will be available for your code and your tests code, and loading your plugin code so you could test it. tests-plugin-sample.php : This file contains the actual tests for your plugin – feel free to rename it.

In order to test WordPress functions, your test classes have to inherit from WP_UnitTestCase class. Other than that, this is a regular PHPUnit test class – so all the regular rules for PHPUnit apply here. You can call any function of your plugin, and of course – the full WordPress SDK is available for you. Also, in WP_UnitTestCase there are a lot of utility methods that will help you in testing your plugin – for example, to generate users or posts. WordPress has a really awesome testing SDK – but the documentation is not so good. There is an unofficial documentation, but you can always read the code.

Now let’s take a look at the bin folder. It contains 2 scripts that install all that is required for our tests. The WordPress CLI generates one file – install-wp-tests.sh , I chose to split it into 2 files so it will play more nicely with Docker – and you’ll understand why when we dive into the Dockerfile .

The last folder is the src folder, which contains the code of your plugin. I chose to move the plugin code to a folder to make things a bit cleaner. The root folder contains many files now, and it is a bit hard to understand what’s going on.

The Dockerfile

Let’s take a look at the Dockerfile , to get a better understanding of what is going on:

ARG PHP_IMAGE_TAG

FROM php:$PHP_IMAGE_TAG

ARG WORDPRESS_DB_PASSWORD

ARG WORDPRESS_VERSION

The first lines of the Dockerfile define a few build arguments that can be used to customize the test environment — the PHP and WordPress version. This allows you to make sure your code is compatible with all the relevant versions. The last argument is the DB password — which is controlled by the compose file: it has to be the same as the one passed to the DB container.

The PHP image has to be based on Alpine Linux — because starting at PHP 5, you need to install an extension in order to interact with MySQL. for some reason, this did not work well on Ubuntu Linux and moving to Alpine Linux solved all the weird MySQL issues. By looking here, it looks like the process of installing extensions works better on Alpine Linux. This is also the reason I did not use the PHPUnit image — it does not come with the docker-php-ext-enable utility that allows you to easily enable PHP extensions.



apk add --update --no-cache subversion mysql mysql-client git bash g++ make autoconf && \

set -ex;&& \

docker-php-ext-install mysqli pdo pdo_mysql pcntl \

&& php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \

&& php composer-setup.php --install-dir=/usr/bin --filename=composer

&& docker-php-source extract \

&& pecl install xdebug \

&& docker-php-ext-enable xdebug \

&& docker-php-source delete \

&& echo "xdebug.remote_enable=on" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \

&& echo "xdebug.remote_autostart=off" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \

&& echo "xdebug.remote_port=9000" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \

&& echo "xdebug.remote_handler=dbgp" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \

&& echo "xdebug.remote_connect_back=0" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \

&& curl -sS

&& rm -rf /tmp/* RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories &&\apk add --update --no-cache subversion mysql mysql-client git bash g++ make autoconf && \set -ex;&& \docker-php-ext-install mysqli pdo pdo_mysql pcntl \&& php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \&& php composer-setup.php --install-dir=/usr/bin --filename=composer&& docker-php-source extract \&& pecl install xdebug \&& docker-php-ext-enable xdebug \&& docker-php-source delete \&& echo "xdebug.remote_enable=on" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \&& echo "xdebug.remote_autostart=off" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \&& echo "xdebug.remote_port=9000" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \&& echo "xdebug.remote_handler=dbgp" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \&& echo "xdebug.remote_connect_back=0" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \&& rm -rf /tmp/*

This chunk installs all that is required to run the setup scripts and our tests. The script uses SVN to download WordPress, and mysqladmin to manage the MySQL DB. You will notice I’m installing a few extensions, mainly the MySQL extension. XDebug is installed to enable code coverage and for remote debugging (see here to learn how to debug over docker. I personally did not try it). And finally, Composer installed to manage dependencies for our test code, including PHPUnit

WORKDIR /tmp

COPY ./bin/install-wp-tests.sh /tmp/install-wp-tests.sh .

RUN /tmp/install-wp-tests.sh wordpress_test root $WORDPRESS_DB_PASSWORD mysql $WORDPRESS_VERSION

COPY ./db-error.php /tmp/wordpress/wp-content/db-error.php .

Those lines take care of installing WordPress and the testing SDK. The installation process takes some time, especially because it downloads a lot of things. That’s why I am running it as soon as possible, so it will be cached and not have to run each time I trigger a build on my machine. The db-error is used so we will have meaningful DB errors, but it is not required.

WORKDIR /wordpress

COPY composer.json /wordpress

RUN composer install

COPY . /wordpress

After all that, all we need to do is copy composer.json and install the dependencies using composer – again, to keep it cached when changing only test files. You’ll notice that I am using a slightly older version of PHPUnit (5.5.0) – there is a breaking change since PHPUnit 6 that broke WordPress test SDK (see here for more details), so I had to use an older version. And finally – copy all other files to the container.

CMD bin/install-db.sh wordpress_test root $WORDPRESS_DB_PASSWORD mysql

You will notice that the CMD is the DB init script: This is why I’ve split the original script into 2 different files. All the WordPress initialization could happen early in the building stage so it will be cached and will not have to be run every time our test code changed. The DB initialization scripts require an actual DB to work with — so it has to be run only after the DB is ready.

Conclusion

I hope that now you have a better understanding of how the template works. I’m already looking forward to hearing about your experience with it: Were you able to use it? What worked and what didn’t work for you? Share your thoughts here in the comments, by opening an issue on the repo — or by tweeting at me (@omerlh).

Originally posted under @soluto-engineering blog