You should probably set up Continuous Integration system
I could go at length about why setting up a “Continuous Something” flow is something you should do, but I’d rather defer the theory and the introduction to this article on Nutcache blog as it provides a clear high-level view of the matter while also providing good starting points for each touched subject.
In a way not dissimilar to what happens with testing the greatest barrier to the adoption and implementation of a CI system is the perceived difficulty.
In the same way that setting up to test code does not mean code coverage has now to be 100% or nothing, setting up to use continuous integration does not mean that everything has to be automated and/or managed by automatic tools.
Continuous integration usually involves running some kind of testing as a first phase; in this post I will focus on how to set up a Nginx web-server on Travis CI to run acceptance and functional like tests.
I will then specialize the guide for a WordPress-based application (e.g. a plugin, a theme or a whole site) and provide two examples.
Installing Nginx on Travis CI
I’ve been using Travis CI, in its free and paid version, for a while now.
The support team proved to be, for private and open-source projects as well, crazy fast in dealing with any request or issue and tutorials, should one need help beyond Travis CI documentation, are abundant on the web.
In the past I’ve relied on an Apache-based setup to scaffold test WordPress installations; that’s a collection of scripts that configure Apache web-server, PHP and install WordPress on a Ubuntu box (the machine provided by Travis CI) to be able to run acceptance tests using a “pretty” URL, like http://wp.dev
.
Due to recent changes to the base Travis CI box, the script is currently not working; out of frustration I tried a different approach using Nginx to see how that would work.
Installing and configuring Nginx proved to be rather easy and is the first step to a fully automated WordPress testing environment.
In the following example I will assume the site to be tested is the repository itself; if this is not true the second, WordPress-specific, example will handle a different case.
Below is a Travis configuration file (.travis.yml
), with copious inline comments, to get me started:
# the setup I'm using requires the use of `sudo`
# to run some commands as super-user, as such I
# require it at the top of the configuration file
sudo: required
# this tells Travis to set up the box with some common tools
# used in PHP projects; while one can start with a "blank"
# box I find this convenient
language: php
# no email notifications for me; this is personal choice
notifications:
email: false
# I want my project to be tested with the following PHP versions
php:
- '7.0'
- '7.1'
- nightly
matrix:
fast_finish: true
# if the tests fail on the "nightly" version of PHP that's fine
allow_failures:
- php: nightly
# I will need to use a database in my tests so, in place of installing and
# setting up a MySQL server I simply require it; this will make the `mysql`
# binary available
services:
- mysql
cache:
apt: true
directories:
- vendor
- $HOME/.composer/cache/files
addons:
# instead of adding a line, in the `before_install` or `install`
# sections to install packages using `apt-get install...` I simply
# delegate the task to Travis
apt:
packages:
# I will need PHP in its FPM version, MySQL PHP extensions and Nginx
# itself
- php5-fpm
- php5-mysql
- nginx
# if I need to resolve pretty URLs to the local machine in place
# of adding a line that tries to modify the `/etc/hosts` file I can
# specify all the modifications I'd like to do here.
# there is no `*.site.localhost` option so I need to add an entry for
# each subdomain I want to use
hosts:
- site.localhost
- test1.site.localhost
env:
# here I define some environment variables I will be able to use
# in scripts and in the PHP application itself
# e.g `$SITE_FOLDER` will return `/tmp/site` in a bash script
# while `$siteFolder = getenv('SITE_FOLDER');` will return the same
# value in a PHP script
global:
- SITE_URL="http://site.localhost"
- SITE_DOMAIN="site.localhost"
- DB_NAME="test"
before_install:
# create the databases that will be used by the site using `mysql` binary
# user is `root`, password is empty and host is `localhost`
- mysql -e "create database IF NOT EXISTS $DB_NAME;" -uroot
install:
# install Composer dependencies
- composer install --prefer-dist
# copy the Nginx configuration file for the site among the available ones from
# the `build` folder; the content of the Nginx configuration file I'm using is
# specific to WordPress (see below) so use whatever you need to use.
# Given the current environment configuration it will be copied to `/etc/nginx/sites-available/site.localhost`
- sudo cp build/travis-nginx-conf /etc/nginx/sites-available/$SITE_DOMAIN
# using sed replace the placeholder text in the Nginx configuration file to use the
# correct site folder path and domain
- sudo sed -e "s?%TRAVIS_BUILD_DIR%?$TRAVIS_BUILD_DIR?g" --in-place /etc/nginx/sites-available/$SITE_DOMAIN
- sudo sed -e "s?%SITE_DOMAIN%?$SITE_DOMAIN?g" --in-place /etc/nginx/sites-available/$SITE_DOMAIN
# enable the site by creating a symbolic link in the `sites-enabled` folder
- sudo ln -s /etc/nginx/sites-available/$SITE_DOMAIN /etc/nginx/sites-enabled/
before_script:
# restart Nginx and PHP-FPM services to make sure the new site is loaded
- sudo service php5-fpm restart
- sudo service nginx restart
script:
# finally run the tests...
- curl http://site.localhost
In the file above I refer to a build/travis-nginx-conf
file; trying to keep this example removed from WordPress any template found on the web will do; I’m reporting here one where I’ve laid out the base for the sed
replacements and some sane testing defaults:
server {
listen 80;
server_name %SITE_DOMAIN% *.%SITE_DOMAIN%;
root %TRAVIS_BUILD_DIR%;
index index.php;
client_body_in_file_only clean;
client_body_buffer_size 32K;
client_max_body_size 300M;
sendfile on;
send_timeout 300s;
location / {
try_files $uri $uri/ /index.php?$args ;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
# Expire images, icons, video, audio, HTC in 1 hour, *pretty* ok in development
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
expires 1h;
access_log off;
add_header Cache-Control "public";
}
# Do not cache CSS and Javascript files at all
location ~* \.(?:css|js)$ {
# set this to `off` to avoid issues with the virtual machine
sendfile off;
expires -1;
access_log off;
add_header Cache-Control "public";
}
}
The WordPress specific example
There are two main differences between the example above and what I use for WordPress projects: 1. I’m not testing the site as a whole but just a plugin or a theme, as such the TRAVIS_BUILD_DIR
folder will contain the plugin or theme code, not the whole site; the site will live in a separate folder 2. I’m using Codeception and wp-browser to run the tests
Here is the modified .travis.yml
template file with more WordPress specific comments:
sudo: required
language: php
notifications:
email: false
php:
- '7.0'
- '7.1'
- nightly
matrix:
fast_finish: true
allow_failures:
- php: nightly
- env: WP_VERSION=nightly
services:
- mysql
cache:
apt: true
directories:
- vendor
- $HOME/.composer/cache/files
addons:
apt:
packages:
# required by WordPress
- libjpeg-dev
- libpng12-dev
- php5-fpm
- php5-mysql
- nginx
hosts:
- wp.localhost
- test1.wp.localhost
- test2.wp.localhost
env:
global:
- WP_FOLDER="/tmp/wordpress"
- WP_URL="http://wp.localhost"
- WP_DOMAIN="wp.localhost"
- DB_NAME="test"
- TEST_DB_NAME="wploader"
- WP_TABLE_PREFIX="wp_"
- WP_ADMIN_USERNAME="admin"
- WP_ADMIN_PASSWORD="admin"
- WP_SUBDOMAIN_1="test1"
- WP_SUBDOMAIN_1_TITLE="Test Subdomain 1"
- WP_SUBDOMAIN_2="test2"
- WP_SUBDOMAIN_2_TITLE="Test Subdomain 2"
matrix:
- WP_VERSION=latest
- WP_VERSION=nightly
before_install:
# create the databases that will be used in the tests
- mysql -e "create database IF NOT EXISTS $DB_NAME;" -uroot
- mysql -e "create database IF NOT EXISTS $TEST_DB_NAME;" -uroot
# set up folders
- mkdir -p $WP_FOLDER
- mkdir tools
# install wp-cli in the `tools` folder
- wget https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -P $(pwd)/tools/
- chmod +x tools/wp-cli.phar && mv tools/wp-cli.phar tools/wp
# append the `tools` folder to the PATH
- export PATH=$PATH:$(pwd)/tools
# prepend the `vendor/bin` folder the PATH
- export PATH=vendor/bin:$PATH
install:
- composer install --prefer-dist
# install WordPress in the `wordpress` folder
- cd $WP_FOLDER
- wp core download --version=$WP_VERSION
- wp config create --dbname="$DB_NAME" --dbuser="root" --dbpass="" --dbhost="127.0.0.1" --dbprefix="$WP_TABLE_PREFIX"
- wp core multisite-install --url="$WP_URL" --base="/" --subdomains --title="Test" --admin_user="$WP_ADMIN_USERNAME" --admin_password="$WP_ADMIN_PASSWORD" --admin_email="admin@$WP_DOMAIN" --skip-email
# flush rewrite rules to use pretty permalinks
- wp rewrite structure '/%postname%/' --hard
# create the two subdomain sites I need
- wp site create --slug="$WP_SUBDOMAIN_1" --title="$WP_SUBDOMAIN_1_TITLE"
- wp site create --slug="$WP_SUBDOMAIN_2" --title="$WP_SUBDOMAIN_2_TITLE"
# update WordPress database to avoid prompts
- wp core update-db --network
# export a dump of the just installed database to the _data folder
- wp db export $TRAVIS_BUILD_DIR/tests/_data/dump.sql
# get back to the build folder
- cd $TRAVIS_BUILD_DIR
# copy the Nginx configuration file to the default site and replace the path and domain
- sudo cp build/travis-nginx-conf /etc/nginx/sites-available/$WP_DOMAIN
- sudo sed -e "s?%WP_FOLDER%?$WP_FOLDER?g" --in-place /etc/nginx/sites-available/$WP_DOMAIN
- sudo sed -e "s?%WP_DOMAIN%?$WP_DOMAIN?g" --in-place /etc/nginx/sites-available/$WP_DOMAIN
# enable the WordPress installation
- sudo ln -s /etc/nginx/sites-available/$WP_DOMAIN /etc/nginx/sites-enabled/
before_script:
- sudo service php5-fpm restart
- sudo service nginx restart
# symbolically link the plugin root folder to the WordPress installation folder
# if you are using wp-browser use the Symlinker extension and skip this
- ln -s $TRAVIS_BUILD_DIR $WP_FOLDER/wp-content/plugins
# build Codeception modules
- codecept build
script:
- codecept run acceptance
- codecept run functional
- codecept run integration
- codecept run unit
The build/travis-nginx-conf
file I use for WordPress projects is this:
map $http_host $blogid {
default -999;
#Ref: http://wordpress.org/extend/plugins/nginx-helper/
#include %WP_FOLDER%/wp-content/plugins/nginx-helper/map.conf;
}
server {
listen 80;
server_name %WP_DOMAIN% *.%WP_DOMAIN%;
root %WP_FOLDER%;
index index.php;
client_body_in_file_only clean;
client_body_buffer_size 32K;
client_max_body_size 300M;
sendfile on;
send_timeout 300s;
location / {
try_files $uri $uri/ /index.php?$args ;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
#WPMU Files
location ~ ^/files/(.*)$ {
try_files /wp-content/blogs.dir/$blogid/$uri /wp-includes/ms-files.php?file=$1 ;
access_log off;
log_not_found off;
expires max;
}
#WPMU x-sendfile to avoid php readfile()
location ^~ /blogs.dir {
internal;
alias %WP_FOLDER%/wp-content/blogs.dir;
access_log off;
log_not_found off;
expires max;
}
# Expire images, icons, video, audio, HTC in 1 hour, *pretty* ok in development
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
expires 1h;
access_log off;
add_header Cache-Control "public";
}
# Do not cache CSS and Javascript files at all
location ~* \.(?:css|js)$ {
# set this to `off` to avoid issues with the virtual machine
sendfile off;
expires -1;
access_log off;
add_header Cache-Control "public";
}
}
It will work for single and subdomain-based multisite installations; a good starting point to write a different one is the Codex and Nginx documentation itself.
Bonus - dynamic Codeception configuration
Codeception allows configuring its suites using dynamic configuration parameters pulled from an environment file or the environment itself.
I’ve taken the habit to create only distribution version of Codeception configuration files (e.g. integration.suite.dist.yml
in place of integration.suite.yml
) to simplify the maintenance and leverage this possibility.
These are the steps to configure Codeception to work with dynamic configuration parameters:
- first require the
vlucas/phpdotenv
library:composer require --dev vlucas/phpdotenv
. - modify each configuration file to use placeholders in the
%PLACEHOLDER%
format, e.g. wp-browser ownacceptance.suite.dist.yml
is this:class_name: AcceptanceTester modules: enabled: - WPBrowser - WPDb - AcceptanceHelper - WPFilesystem config: WPBrowser: url: '%WP_URL%' adminUsername: 'admin' adminPassword: 'admin' adminPath: '/wp-admin' WPDb: dsn: 'mysql:host=%DB_HOST%;dbname=%DB_NAME%' user: %DB_USER% password: %DB_PASSWORD% dump: 'tests/_data/dump.sql' populate: true cleanup: true reconnect: false url: '%WP_URL%' tablePrefix: 'wp_' WPFilesystem: wpRootFolder: '%WP_ROOT_FOLDER%' themes: '/wp-content/themes' plugins: '/wp-content/plugins' mu-plugins: '/wp-content/mu-plugins' uploads: '/wp-content/uploads'
- in the distribution version of the main Codeception configuration file,
codecept.dist.yml
specify that dynamic parameters shoudld be used:actor: Tester paths: tests: tests log: tests/_output data: tests/_data helpers: tests/_support settings: # ... wpFolder: '%WP_ROOT_FOLDER%' extensions: # ... params: - .env
- override just the
params
parameter in the local Codeception configuration file,codecpetion.yml
(which is not pushed to the remote repository):params: - .env.local
- create the
.env
and.env.local
files to configure the suites; the.env.
file will be used by the CI host while the.env.local
file will be used locally; as an example here are mines for wp-browser; the.env
one first:WP_DOMAIN="wp.localhost" WP_URL="http://wp.localhost" DB_HOST="localhost" DB_NAME="test" DB_USER="root" DB_PASSWORD="" WP_ROOT_FOLDER="/tmp/wordpress" TEST_DB_NAME="wploader" PHANTOMJS="/usr/local/phantomjs/phantomjs" wpSubdomain1="test1" wpSubdomain1Title="Test Subdomain 1" wpSubdomain2="test2" wpSubdomain2Title="Test Subdomain 2"
- and the
.env.local
one:WP_DOMAIN="wp.localhost" WP_URL="http://wp.localhost" DB_HOST="db.localhost" DB_NAME="wp" TEST_DB_NAME="wpTests" DB_USER="root" DB_PASSWORD="root" WP_ROOT_FOLDER="/Users/luca/Repos/wp-browser/docker/www" PHANTOMJS="/usr/local/bin/phantomjs" wpSubdomain1="test1" wpSubdomain1Title="Test1" wpSubdomain2="test2" wpSubdomain2Title="Test2"
See it in action
I’ve shown, in the post, working code but wp-browser configuration file is available for inspection; since that contains a number of “meta” concepts (testing a WordPress testing tool) a clearer example might be the one provided by the “I’d like this” plugin.