WordPress Version Dependent Codeception Tests

At iThemes we officially support the latest and previous WordPress version for our plugins. This means we end up running our automated test suite across three versions, for instance right now we’re testing against WordPress 5.4, 5.5 and 5.6-beta.

Sometimes when WordPress makes a change, we need to write tests to ensure compatibility. But it only makes sense to run these tests when we’re testing on that new WordPress version. If you are running unit tests this can be fairly straightforward.

public function testNewWpFeature() {
	if ( version_compare( $GLOBALS['wp_version'], '5.5', '<' ) ) {
		$this->markTestSkipped( 'WP 5.5 Required' );
	}

	// Run our test
}

But what if we’re running an acceptance test using Cest? We don’t have immediate access to the WordPress version so we’d need to make some external call to get this information. Even if we could, we don’t want to clutter each of our tests with this repeated logic that isn’t related to our tests.

PHPUnit lets us do something like this using the @requires annotation, but it only supports PHP, PHPUnit and Extensions.

/**
 * @requires PHP >= 5.6
 */
public function testNewWpFeature() {
	// Run our test
}

As far as I know, this would also only work out of the box for our Unit tests, not Cest. Luckily, we can write our own as a Codeception Module!

Writing the module

I’m going to skip the basics of writing a Codeception module in this post. But if you haven’t done it before, check out my previous article which goes into more detail.

To start with, we’ll define a WpVersion class to contain our Module. This will typically be stored in your tests/_support/Helper directory, but can go wherever you’d like.

namespace Helper;

class WpVersion extends \Codeception\Module {

	use \tad\WPBrowser\Traits\WithWpCli;

	private $version;

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

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

We’re using WP-Browser’s built in WithWpCli trait to simplify using WP-CLI in our module. Next we’ll create a helper function for running WP-CLI commands.

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;
}

Now we’ll define our _beforeSuite method. This runs once before any of our tests are executed which makes it a great place to check for the current WordPress version we’re running as.

public function _beforeSuite( $settings = [] ) {
	if ( isset( $GLOBALS['wp_version'] ) ) {
		$this->version = $GLOBALS['wp_version'];
	} else {
		$this->version = trim( $this->runCommand( 'core', 'version' )->getOutput() );
	}
}

We check if the $wp_version global is defined to account for when we’re running WordPress Unit tests. For all other instances, we retrieve the WordPress version using WP CLI. Finally, we need to define our _before method which does the actual work of our module.

Codeception calls the _before method whenever the test.before event is fired. This takes place before each Codeception test executes. The method is passed the current TestInterface object which contains information about the test about to run.

public function _before( \Codeception\TestInterface $test ) {
	if ( ! $requires = $test->getMetadata()->getParam( 'requires' ) ) {
		return;
	}
}

First we check if any requirements are defined in the PHPDoc Block by using the Metadata::getParam() method. This returns an array of @requires annotations. Next, we’ll iterate over that array and check that the annotation is properly formatted.

$pattern = '/^WP\s+(?P<operator>[<>=!]{0,2})\s*(?P<version>[\d\.-]+)$/';

foreach ( $requires as $require ) {
	if ( ! preg_match( $pattern, $require, $matches ) ) {
		throw new \PHPUnit_Framework_RiskyTestError( 
			'Invalid @requires annotation.'
		);
	}
}

If the annotation does not match, then we throw a special PHPUnit exception to mark that this is a risky test. Lastly, we’ll perform the actual version comparison to determine whether or not this test should be run.

if ( ! preg_match( $pattern, $require, $matches ) ) {
	throw new \PHPUnit_Framework_RiskyTestError(
		'Invalid @requires annotation.'
	);
}

$operator = $matches['operator'];
$version  = $matches['version'];

list( $wp_version ) = explode( '-', $this->version );

if ( ! version_compare( $wp_version, $version, $operator ) ) {
	throw new \PHPUnit_Framework_SkippedTestError( 
		sprintf( 
			'WP %s %s is required.',
			$operator,
			$version
		)
	);
}

First we extract the operator and version number from the preg_match() results. Then we transform the version string from something like 5.5.3-alpha-49449 to 5.5.3. This is important because the version_compare() function assumes that pre-release software has a lower version number than the finalized release version. Meaning, that if we said run this test on >= 5.5.3 it wouldn’t run until 5.5.3 has exited development which would be too late to catch any issues.

Lastly, we call the version_compare() function, and if it doesn’t return true we throw another special PHPUnit exception. But this time, we use the one to indicate that this function should be skipped.

Using our module

Make sure to include the new module in your test suite’s yaml file. For example:

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

Now we can add an annotation like this whenever we want to run a test for specific PHP versions!

/**
 * @requires WP >= 5.6
 */
public function testNewWpFeature() {
	// Run our test
}
/**
 * @requires WP >= 5.6
 */
public function seeNewWpVersionWorks( AcceptanceTester $I ) {
	// Run our test
}

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.