Running Codeception tests on WordPress Trunk

At iThemes we use Codeception / WP-Browser to run our automated tests. I’ve described our setup in more detail in an earlier blog post, but the important bit to know for this post is that we use the Official WordPress Docker image as the base of our tests.

The WordPress docker image only creates tags when new versions are officially released which made running our tests on beta releases difficult. To solve this, we need to figure out a way to upgrade the installed WordPress version before our tests run. There are a couple of ways we could go about this.

  1. Add our own trunk Docker image. This is probably the “best” solution in terms of performance and doing things the “right” way. But maintaining our own Docker image that is refreshed nightly is a bunch of work I didn’t want to get into.
  2. Right now, the only volume we mount is our plugin into the plugins directory. We could download a copy of WordPress and then mount WordPress as a volume inside the Docker container. But I think this will also get complicated to deal with installation and wp-config.php generation.
  3. Use WP-CLI to upgrade WordPress to trunk. This is as simple as running wp core update --version=nightly and is officially supported.

Originally, I tried running this in the Bitbucket Pipelines but that of course won’t work since we have to be in the Docker container. Annoyingly, the Official WordPress image doesn’t include WP-CLI in the WordPress container, you have to use a separate image for it. Again, another mess I didn’t want to deal with.

All is not lost, though. WP-Browser includes WP-CLI as a dependency, so we can use that instead by making a Codeception module!

Building the module

To do this, we create a new class that extends the \Codeception\Module class. We can name it whatever we want, and use whatever namespace are project is using. I ended up creating a class called WpVersion in the \Helper namespace. Typically, you’ll want to put this file in the tests/_support/Helper directory.

namespace Helper;

class WpVersion extends \Codeception\Module {

}

WP-Browser helpfully defines a WithWpCli trait that makes interacting with WP-CLI simple. We just need to provide it with the WordPress root directory and it takes care of the rest.

class WpVersion extends \Codeception\Module {

	use \tad\WPBrowser\Traits\WithWpCli;

	public function _initialize() {
		parent::_initialize();

		$this->setUpWpCli( $this->config['path'] );
	}
}

This will take the path that we provide when configuring the module and pass it to the setUpWpCli method from WithWpCli.

Next let’s define a runCommand method that encapsulates the logic for running a WP CLI command and printing debug output. This method will take a list of command arguments and return the Symfony Process instance that WP-Browser uses to run the command.

protected function runCommand( ...$user_command ) {
	$options = [];

	if ( ! empty( $this->config['allow-root'] ) ) {
		$options[] = '--allow-root';
	}

	$command = array_merge( $options, $user_command );

	$this->debugSection( 'WpVersion', $command );

	$process = $this->executeWpCliCommand( $command, null );
	$out     = $process->getOutput() ?: $process->getErrorOutput();
	$this->debugSection( 'WpVersion', $out );

	return $process;
}

This method adds support for specifying allow-root in the Module’s configuration. Running WP-CLI as root in a real WordPress site is a bad idea as it gives all code in your WordPress install root access to your server. However, the WordPress Docker image doesn’t play nicely when not running as root and since we trust our own plugin, I’m not too concerned about it.

Finally we log the command we’re running, execute the command, and log the output.

Now let’s write a method that installs the requested WordPress version called install.

protected function install( $version ) {
	$this->debugSection( 'WpVersion', sprintf(
		'Installing WordPress %s', $version
	) );

	try {
		$process = $this->runCommand(
			'core',
			'update',
			'--version=' . $version
		);

		if ( ! $process->isSuccessful() ) {
			throw new \Codeception\Exception\ModuleException(
				$this,
				sprintf(
					'Failed to install WordPress %s: %s',
					$version,
					$process->getErrorOutput() ?: $process->getOutput()
				)
			);
		}

		echo sprintf( 'Installed WordPress: %s', $version ) . PHP_EOL;
	} catch ( \Exception $e ) {
		throw new \Codeception\Exception\ModuleException(
			$this,
			$e->getMessage()
		);
	}
}

The $version parameter can be any version that we can pass to WP-CLI. For example nightly or 5.5. First we build our command arguments, excluding the leading wp. By passing the command as an array, the Symfony Process class can automatically escape each argument, so we don’t need to manually call escapeshellarg.

We add some additional debug output and throw a ModuleException if the command wasn’t successful. Lastly, we echo some output directly to the screen indicating what WordPress version we installed.

To put it all together we need to listen for the suite.before Codeception event. This runs once immediately before a Codeception suite starts to run tests. Helpfully, Codeception Modules get easy access to this event by implementing the _beforeSuite method.

public function _beforeSuite( $settings = [] ) {
	if ( ! empty( $this->config['version'] ) ) {
		$this->install( trim( 
			$this->config['version'],
			" \t\n\r\0\x0B'\""
		) );
	}
}

This checks for a version configuration flag, and if one is present we pass it to the install method we wrote earlier. We also apply trim to the defined version to get rid of any whitespace or quotes.

Configuring the module

To use and configure our module we need to add it to our suite’s .yml file. For instance your acceptance.yml file might look something like this.

actor: AcceptanceTester
modules:
  enabled:
    - WPDb
  config:
    WPDb:
      url: '%TEST_SITE_WP_URL%'
      urlReplacement: true
      tablePrefix: '%TEST_SITE_TABLE_PREFIX%'

To add our custom module we need to the list the fully-qualified class name to the enabled list.

actor: AcceptanceTester
modules:
  enabled:
    - WPDb
    - \Helper\WpVersion

And then we can specify our configuration in the config section.

actor: AcceptanceTester
modules:
  enabled:
    - WPDb
    - \Helper\WpVersion
  config:
    WPDb:
      url: '%TEST_SITE_WP_URL%'
      urlReplacement: true
      tablePrefix: '%TEST_SITE_TABLE_PREFIX%'
    \Helper\WpVersion:
      version: '%WP_VERSION%'
      path: '%WP_ROOT_FOLDER%'
      allow-root: true

I’ve defined our configuration to utilize two environment variables that we define in our codeception.env file. This let’s us run sed -i -e 's/WP_VERSION=""/WP_VERSION="nightly"/g' tests/codeception.env in our bitbucket-pipelines.yml script to have a step that is setup to run Trunk.

I hope you’ve found this useful. You can checkout the Gist to see the completed Module.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.