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.
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
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:
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="[email protected]$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
build/travis-nginx-conf file I use for WordPress projects is this:
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
composer require --dev vlucas/phpdotenv.
- modify each configuration file to use placeholders in the
%PLACEHOLDER%format, e.g. wp-browser own
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.ymlspecify 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
paramsparameter in the local Codeception configuration file,
codecpetion.yml(which is not pushed to the remote repository):
params: - .env.local
- create the
.env.localfiles to configure the suites; the
.env.file will be used by the CI host while the
.env.localfile will be used locally; as an example here are mines for wp-browser; the
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
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"
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.