ūüźč Test your WordPress themes and plugins with Docker

In this article I’ll explain¬†how Docker can simplify testing, when engineering WordPress components. But before I even dive into WordPress testing with Docker, let’s just go through some basic stuff, to make sure we’re on the same page:

TL;DR? Click here to skip to the how-to part.

A list of lists of things to worry about as a WordPress developer

So you’ve built your theme or plugin and “It Works‚ĄĘ”.

Being a pro ninja developer, you’ve thought of, analyzed, designed, developed, and tested, everything. Here are some issues you’ve had to address, in no particular order:

  • Your UI has been optimized with respect to the user experience. Any Settings pages or Customizer tabs are in place and easy to navigate. Perhaps you’ve even made use of the¬†Admin¬†Pointers API. Good job! Your efforts will mean less effort in support later on.
  • You’ve interfaced with the right WordPress APIs for the job, using recommended practices. Perhaps you’ve extended the TinyMCE editor to add a button or two for your special functionality. Perhaps you’ve even catered¬†for people who use WordPress with the CKEditor. Or perhaps you’ve decided that they’re not worth the extra effort. In any case, you’ve made your educated guesses in your analysis, and you have followed through in your implementation.
  • You’ve made sure that your product plays¬†nicely with prominent plugins and themes in the wild jungle that is the WordPress ecosystem. You’ve tested your deliverable side-by-side with titans such as Visual Composer, WooCommerce and the likes, and you’re satisfied with the synergy.
  • You’ve made¬†correct use of¬†HTML5, and CSS3, while silently serving polyfills to the Internet Explorer lusers and anyone still¬†browsing the web on¬†an Android Gingerbread.
  • You’ve used¬†the built-in caching mechanisms appropriately. Remember, cache invalidation is one of the only two hard problems in Computer Science. You’ve made sure that power users can tweak your cache setting, while novices don’t need to know or care.
  • If you are creating your own tables on the database, you have a plan in place for upgrading the schema in future versions. Perhaps you’re saving the installed version number on the wp_options table? Good job! Very pro move!
  • You’ve hooked into plugin_activation_hook() and friends. You’ve come to terms with the humbling fact that users might, at some point, and for whatever reason, choose to uninstall your product! You’re kind enough to clean up after yourself. The community thanks you.
  • Your slugs don’t clash with any other well-known slugs. Also, your post types don’t clash with other known post types. Remember, naming things is the other hard problem in Computer Science! Congratulate yourself for having tackled it, within the context of your project.
  • Your design reflects the WordPress mantras. Study them here.
  • You’ve thought about code quality. Your code conforms to the PHP and JavaScript WordPress coding standards. You only use the good parts of JavaScript. Also, your code is readable and idiot-proof enough that even a non-programmer can hack it without breaking too much stuff. Perhaps you’ve incorporated PHP_CodeSniffer, JSHint and friends into your IDE or build workflow. Bonus points for using PHPdoc comments on any APIs you expose.
  • You’ve invested time in tooling/automation. Perhaps you use grunt or gulp or some other modern task management tool to automate testing, packaging (code, assets, translations, documentation, etc), deployment; you know, all the boring stuff. Perhaps you’re managing your dependencies with composer. The bottom line here is: you understand that shorter and easier iterations make you a more productive and insightful developer. If you can test and build a new release with one click, pat yourself on the back! You’re amazing and your effort will save you heaps of trouble later on.
  • You’ve done at least something about testing. Perhaps you’ve decided your plugin’s functionality is simple enough that some smoke testing is enough. Or you have a rigorous “script” for the human that tests functionality via the browser. Bonus points if you’ve used a solution such as PHPUnit, perhaps with some WP_Mock on the side. Or perhaps you’ve even gone as far as doing automated browser testing on real devices and browsers, using karma or some other bleeding-edge solution. You’re insane! I love you!
  • To double-check you’ve thought of everything, you’ve scoured the web for other listicles of last-minute things to check. Here’s an example. Go find more.
  • If it’s a contract job, your client is happy. If it’s not, you’ve done your corridor testing and other WordPress developers or end users have given you their feedback.

(Version) hell is other programs

If you’ve tackled all of the above and more, surely you’re “done”, right?

WRONG! Get ready to go through your test scenarios and checklists again on WordPress 4.0.10. And what about WordPress 3.7.13? Perhaps it’s too old to support, but you want to know if you can make the claim that your product works on it, right? And all of this has to work on PHP 5.4. Perhaps also on PHP 5.3? If you’ve done any custom DB stuff, do your SQL queries work as expected on MySQL 5.5? How about MySQL 4.x? And for every one of these version combinations, your product must work on the multitude of browsers and devices that you’re testing on. Oh, my!

Every line of code you’ve written is a potential point of failure on some combination of platform, user data, and other pieces of software that share the same platform and data. Programming is easy, software engineering is hard!

Remember, on day 1 when you go live, you will have a number of bugs coming in from users who use your product in combination with themes and plugins you haven’t even thought of. Your code will run on server environments so stupidly configured, you’ll want to find out where their administrator lives just so you can go punch them in the face. No matter how large or small your dev team is, you’ll have to tackle bugs very quickly in the first few days. On the internet, first impressions matter. So:

The more bugs you can identify before your users do, the better for the reputation of your product and team.

The good news is that if you’ve been proactive with your tooling and test automation (and, being a professional developer, why wouldn’t you?), the easier and quicker it will be to repeat your tests on different platforms. But you still need to setup these platforms and deploy and test your product on them. Doing this using Virtual Machines is ultra-tedious, error-prone, and slow.

Enter docker

The age-old problem of software development can be summarized in the phrase “But it worked on my computer”. Today this problem has a solution, and it is called Docker. If you’ve been living under a rock for the past few years, go read what Docker is and how it works before we move on. Go on, I’ll wait here!

Traditionally, lightweight containers are used like so: the developer “dockerizes” their product and delivers the resulting container to the system administrator, thus simplifying the deployment phase as well as any software migrations. Because deployment instructions are written by the developer in an unambiguous, repeatable, machine-executable form, this also eliminates human error and the aforementioned need for anyone to punch anyone in the face.

This is not what we’ll do here. In the context of WordPress component development, we will use a lightweight container to test, not deploy.

What I did (and how you can do it too)

Let me just come out and say it: I do not know, or want to know, how to install several versions of PHP side-by-side on one system. There is a multitude of how-to articles on the topic. I hate all of them because I do not understand Linux package management their authors are all stupid and lame. Instead, I started from this article¬†about a multi-PHP Docker image. You can optionally go read it, if you promise to come back here once you’re finished :-p

The bottom line is that there is a Docker image that already does half of what we want: It provides an environment where we can easily switch between a reasonable selection of popular PHP versions.

I then extended the image so that it installs WordPress, and my plugin, and runs it.

My plugin does not directly access the MySQL database. I only store data using the Options API. Therefore I ran a single version of a MySQL docker container like so:

$ docker run --name wordpress-mysql -e MYSQL_ROOT_PASSWORD=password -d mysql:5.5

Now that our MySQL 5.5 database is up and running, let’s write a Dockerfile that describes an image based on that base image:

FROM eriksencosta/php-dev:latest

It is traditional to include some metadata:

MAINTAINER Alexandros Georgiou <alex.georgiou@gmail.com>
LABEL Description="Container for testing themes and plugins on all the PHPs and WordPresses" Version="1.0"

Then I declare two environment variables where I set the desired PHP and WP versions for my test. Note the use of the ENV instructions. (We can later override these values from the command line without the need to rebuild the image).

ENV WP_VER=4.0
ENV PHP_VER=5.3

We will want to be able to use php, phpenv, composer, etc. so let’s set the environment’s PATH to something sensible:

ENV PATH /opt/phpenv/shims:/opt/phpenv/bin:/opt/php-build/bin/:/opt/composer/vendor/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Now let’s use the WORKDIR instruction to say to docker that we want to do some stuff in the root user’s home directory. This is analogous to the cd command.

WORKDIR /root

Here we will download and install the WP-CLI.

Having a command line interface to our WordPress instance is essential because we need an automated way (i.e. script) to deploy and test our software. (If you are a WordPress developer and you do not know or are not already using WP-CLI, please stop reading this blog, step away from the computer, sell your computer, and change your career to something other than engineering.) Part of being an engineer is about doing things in an automated, repeatable and testable way.

Because my project is based on grunt, I use apt-get to install node and npm. This will allow me to run stuff like npm install and grunt test later on. I also install some basic utilities, so when a bug shows up, I can drop into the container’s shell and investigate. Here I installed the MySQL client, Midnight Commander (my favorite orthogonal file manager), and two text editors, vim and nano. Depending on your project’s needs and personal preferences you will probably want to have other things installed via apt-get, but I bet you will need wp-cli.

RUN \
    curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
    chmod +x wp-cli.phar && \
    mv wp-cli.phar /usr/local/bin/wp && \
    curl -sL https://deb.nodesource.com/setup | bash - && \
    apt-get install -y nodejs npm mysql-client vim nano mc

Note the backslashes. We’ve stringed together the shell commands into one as a performance optimization. Every command creates a new intermediate image in docker. We could just as well have issued commands in separate RUN instructions, but we know better than that!

The following ADD instruction says that any files in the same directory as this Dockerfile are to be copied into the image’s root user home directory.

ADD . /root

The way I’ve set things up, this will bring in a script that I call install.sh. This will carry out the deliverable’s deployment and possibly add content and run any automated testing we have available. Remember that the CMD instuction defines the utility command of the container. You can later override this from the command line. This is very useful for debugging.

CMD /bin/bash -c /root/install.sh

In that same home directory I have a plugins directory. This will contain the zip files for any plugins I want to install. My themes directory will contain the themes that I will want to install. I have a wp directory where I have downloaded tarballs of every WordPress version since 3.7. Adding all of the versions into the image, we make sure that we can use our environment variable to switch between images without rebuilding the docker image. Arguably one could use the wp-cli core install command but that would involve re-downloading stuff every time.

Of course we need to make sure that we can access the image’s nginx web server:

EXPOSE 80

Let’s look at the entire Dockerfile again before we dive into¬†install.sh:

FROM eriksencosta/php-dev:latest

MAINTAINER Alexandros Georgiou <alex.georgiou@gmail.com>
LABEL Description="Container for testing themes and plugins on all the PHPs and WordPresses" Version="1.0"

ENV WP_VER=4.0
ENV PHP_VER=5.3

ENV PATH /opt/phpenv/shims:/opt/phpenv/bin:/opt/php-build/bin/:/opt/composer/vendor/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

WORKDIR /root

RUN \
    curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
    chmod +x wp-cli.phar && \
    mv wp-cli.phar /usr/local/bin/wp && \
    curl -sL https://deb.nodesource.com/setup | bash - && \
    apt-get install -y nodejs npm mysql-client vim nano mc

ADD . /root

CMD /bin/bash -c /root/install.sh

EXPOSE 80

Neat!

We then start writing our install script. Now, dockerfiles are a relatively new thing and warrant some explanation, but fortunately we all speak fluent bash, so I hopefully don’t need to say much about the following code.

Remember how we’ve set environment variables for WordPress and PHP? Now is the time to use them.

#!/bin/bash -x

echo "Installing on WordPress ${WP_VER} on PHP ${PHP_VER}"

phpenv global $PHP_VER

We’ve switched to the right PHP, now on to install the right version of WordPress from our directory of WordPress tarballs:

cd /var/www

tar xfv /root/wp/wordpress-${WP_VER}.tar.gz
touch /var/www/wordpress/wp-content/debug.log
chown -R root:root /var/www/wordpress

Next, we create the wp-config.php file, with the help of WP-CLI. We will want to enable logging, to spot any errors resulting from running our code on different versions. We will also expose the image’s 80 port to port 81 of our local machine. This is necessary if you are already running a web server on your dev machine. Change 81 to any port that you have free.

cd /var/www/wordpress

wp core config --allow-root --dbhost=db --dbname=wpdb --dbuser=root --dbpass=password --extra-php <<PHP
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define('WP_HOME','http://localhost:81/wordpress');
define('WP_SITEURL','http://localhost:81/wordpress');
PHP

Throughout my script you’ll notice the liberal use of --allow-root in wp-cli commands. In our container we do stuff as the user root, hence the need for the switch.

Let’s make sure that we start with a clean database:

wp db drop --allow-root --yes

wp db create --allow-root

wp core install \
    --allow-root \
    --url=http://localhost:81 \
    --title="Testing on WordPress ${WP_VER} on PHP ${PHP_VER}" \
    --admin_user=admin \
    --admin_password=password \
    --admin_email=alex.georgiou@gmail.com

Now let’s install the plugins we want in our test:

wp plugin install /root/plugins/*.zip --allow-root
wp plugin activate --all --allow-root

Perhaps you’ll need to import some content into your WordPress instance. Here we import a post that could contain new shortcodes or whatever:

wp post create --allow-root --post_status=publish --post_type=page --post_title='Test post' /root/post.txt

Depending on your project’s needs, you can go wild here. Use wp-cli to tweak your test site as needed. This is also where you can run your automated tests.

mkdir /root/src
cd /root/src
git clone http://dev.lan/git/myproject.git
cd myproject
npm install
grunt phpunit

Now let’s make sure that nginx is running, and that we are viewing the debug logs and web server logs:

webserver restart
tail -f /var/log/nginx/*.log /var/www/wordpress/wp-content/debug.log

That’s it!

Let’s review the install script in its entirety:

#!/bin/bash -x

echo "Installing on WordPress ${WP_VER} on PHP ${PHP_VER}"

phpenv global $PHP_VER

cd /var/www

tar xfv /root/wp/wordpress-${WP_VER}.tar.gz
cp /root/.htaccess /var/www/wordpress
touch /var/www/wordpress/wp-content/debug.log
chown -R root:root /var/www/wordpress

cd /var/www/wordpress

wp core config --allow-root --dbhost=db --dbname=wpdb --dbuser=root --dbpass=password --extra-php <<PHP
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define('WP_HOME','http://localhost:81/wordpress');
define('WP_SITEURL','http://localhost:81/wordpress');
PHP

wp db drop --allow-root --yes

wp db create --allow-root

wp core install \
    --allow-root \
    --url=http://localhost:81 \
    --title="Testing on WordPress ${WP_VER} on PHP ${PHP_VER}" \
    --admin_user=admin \
    --admin_password=password \
    --admin_email=alex.georgiou@gmail.com

wp plugin install /root/plugins/*.zip --allow-root

wp plugin activate --all --allow-root

wp post create --allow-root --post_status=publish --post_type=page --post_title='Test post' /root/post.txt

mkdir /root/src
cd /root/src
git clone http://dev.lan/git/myproject.git
cd myproject
npm install
grunt phpunit

webserver restart

tail -f /var/log/nginx/*.log /var/www/wordpress/wp-content/debug.log

Beautiful!

We’re already running one docker image with a standard MySQL database. Now we need to build our webserver image and run it.

First, build the image:

docker build -t alexg/wp-test:latest .

Then, run it, making sure to link the database image, map the 80 port to local 81, and set the version variables:

docker run --link wordpress-mysql:db -p 81:80 -e PHP_VER=5.6 -e WP_VER=4.4.2 -i -t alexg/wp-test:latest

This command will now fire up a WordPress version of your choosing, and run the install script. The script will install your theme or plugin, add content, run your automated tests, and finally expose the entire thing at http://localhost:81/wordpress. Visit http://localhost:81/wordpress/wp-login.php and login with admin/password or whatever credentials you’ve set in the wp-cli core install command.

Your shell should now be tailing the nginx and WP logs. Do any automated or manual testing via your browser(s), then hit Ctrl-C to stop the container and run the command with different versions, like so:

docker run --link wordpress-mysql:db -p 81:80 -e PHP_VER=5.3 -e WP_VER=4.0 -i -t alexg/wp-test:latest

Hit http://localhost:81/wordpress again and voila! Your product is now running on a completely different environment.

Now say you’ve spotted an error that did not show¬†in your development environment. It is probably a version-related issue. How do you investigate?

You could override the install script by setting the utility command to be the Linux shell (note the part in bold):

docker run --link wordpress-mysql:db -p 81:80 -e PHP_VER=5.6 -e WP_VER=4.4.2 -i -t alexg/slate-test:latest /bin/bash

Now docker loads your image and drops you in a shell. Here you can do fun stuff like connect to your database via the command line (remember, we’ve installed the mysql-client package):

mysql -h db -u root -p
use wpdb;

Once you’re ready you can run the install script yourself, or even run the deployment manually if you need to tweak stuff.

/root/install.sh

That’s it!

Conclusion

Thanks to Docker, we’ve managed to parametrise our test system to the point where we know for sure that a particular deliverable will behave in a combination of PHP and WordPress versions. Want to test on different MySQL versions? Easy. Just tweak the command where we fire up the database.

You might think that all of this is too much. You might think that testing on a couple of versions is OK. But you would be wrong. You’ll be amazed at what issues crop up when you do rigorous testing. For instance, I was using shortcodes where their attributes had dashes (-) in their names. This is fine in WordPress 4.3.0 and greater, but not in earlier versions. Thanks to docker testing, I was able to find this out and fix it by changing any dashes to underscores. And I was able to do all this, without having to read this part of the documentation that expressly warns about the issue:

Attribute names are optional and should contain only the following characters for compatibility across all platforms:

  • Upper-case and lower-case letters: A-Z a-z
  • Digits: 0-9
  • Underscore: _
  • Hyphen: – (Not allowed before version 4.3.0)

In conclusion, does my solution suck? Yes, it sucks bad, because this was my first real use of Docker. Being a lightweight container “rookie”, I’ve probably done more than a few things “The Wrong Way”. Feel free to flame away in the comment section below, preferably but not necessarily in a constructive manner.

But, did¬†my solution¬†work? You bet your computer’s rear USB port it did!

Or, at least, it worked on my machine :-p

2 thoughts on “ūüźč Test your WordPress themes and plugins with Docker

Leave a Reply

Your email address will not be published. Required fields are marked *