A WordPress plugin I’ve been working on recently needed to be able to accept configuration in a few different ways: users should be able to define a constant in the wp-config.php
file or fill out a form within a settings screen. If the constant is defined, the setting screen should be aware of that and hide the setting, since the constant should take precedence.
This is a pretty common pattern in WordPress plugins, but it can get rather tricky to test; by design, once a constant is defined in PHP, you shouldn’t be able to change its value. PHPUnit has ways to work around this by running tests that define constants in separate processes, but this can seriously impact the performance of your test suite. Furthermore, the WordPress core test suite is pretty tightly coupled, so it doesn’t like when tests are run separately.
Enter Runkit7
An alternative way to deal with tests that require redefining constants and/or functions is an approach known as “monkey-patching”, where we’re literally re-defining behavior at runtime:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function doSomething() { echo 'Foo'; } doSomething(); #=> Foo runkit_function_redefine('doSomething', function () { echo 'Bar'; ]); doSomething(); #=> Bar |
That’s pretty cool, huh? It’s also pretty frightening — the ability to redefine functionality at runtime is rarely a good idea. As a general rule, if you’re monkey-patching in a production environment, you’re probably doing it wrong.
The official PHP extension for monkey-patching has been Runkit, but the package has yet to add official support for PHP 7.0+. An unofficial fork, dubbed “Runkit7”, is working to add PHP 7+ support to Runkit and, from what I’ve seen, it appears to be working well.
Using Runkit7, we might define a constant at the beginning of our test, then clean it up at the end; this prevents the constant being defined here from impacting other tests in the suite:
1 2 3 4 5 6 7 8 9 10 11 12 |
public function testConfigurationViaConstant() { define('MYPLUGIN_CONFIG', 'foobar'); // Test your plugin based on that config. /* * Clean up the defined constant so it won't interfere * with other tests. */ runkit_constant_remove('MYPLUGIN_CONFIG'); } |
Better yet, we can add a method to our test suite using PHPUnit’s @after
annotation to automatically have it run after every test that have set the value. This will ensure that even if an assertion in our test method fails the constant will still be removed at the end of the test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * Clean up the MYPLUGIN_CONFIG constant after each test method. * * @after */ public function removeConstants() { if (function_exists('runkit_constant_remove') && defined('MYPLUGIN_CONFIG')) { runkit_constant_remove('MYPLUGIN_CONFIG'); } } /** * @requires extension runkit */ public function testConfigurationViaConstant() { define('MYPLUGIN_CONFIG', 'foobar'); // Test your plugin based on that config. } |
You’ll notice that our testConfigurationViaConstant()
method also includes a @requires extension runkit
annotation — this tells PHPUnit that the test method shouldn’t be executed unless Runkit is active in the test environment. If Runkit (and its functions) are unavailable, PHPUnit will simply mark the test as skipped and move on.
Generally, if I have a test suite that might need to skip tests due to Runkit being missing, I’ll indicate that with the following line in my test suite’s bootstrap file:
1 2 3 |
if (! function_exists('runkit_constant_remove')) { echo "\033[0;33mWARNING: Runkit is not active in the current environment, so not all tests can be run.\033[0;0m" . PHP_EOL; } |
Of course, it would be nice if there was a way to automatically install Runkit7 in a development or testing environment, wouldn’t it?
Automatically install Runkit7 in a development or testing environment
When I found out about Runkit7, I immediately wanted a way to be able to specify it as a development dependency via Composer. While installation via PECL isn’t particularly difficult, it’s nice to be able to script the installation. The result is my runkit7-installer Composer package.
To install it into your project, simply install via Composer:
1 |
$ composer require --dev stevegrunwell/runkit7-installer |
Once installed, running the vendor/bin/install-runkit.sh
file will automatically attempt to install and activate Runkit7 in your local environment, making it easier than ever to use it in your projects!
For instance, Runkit7 might be used in your Travis CI builds with a travis.yml
file that looks something like this:
1 2 3 4 |
install: - composer install --prefer-dist - vendor/bin/install-runkit.sh # Any other installation steps you might need |
Hopefully this helps someone else!
Leave a Reply