Filtering WordPress from “outside” WordPress.
The problem
The recent addition, in the WPLoader
module, of the possibility to “only load” WordPress installations has opened many doors; these doors grant access to functions, classes and methods defined by WordPress itself, plugins, and themes, in the same variable scope as the tests.
Alas, this new power notwithstanding, the test below will fail in the second $I->see
assertion:
$I = new WpfunctionalTester($scenario);
$I->wantTo('verify the post title can be filtered');
$postId = $I->havePostInDatabase(['post_title' => 'Original title']);
$I->amOnPage("/index.php?p={$postId}");
$I->see('Original title');
add_filter('the_title', function(){
return 'Filtered title';
});
$I->amOnPage("/index.php?p={$postId}");
$I->see('Filtered title');
The test above could be running in an acceptance or functional test case that’s relying on the WPDb
and WPBrowser
modules; the former to manipulate the database ($I->havePostInDatabase
) and the second to get around the WordPress site as a user would ($I->amOnPage
and $I->see
).
The reason the second assertion is failing is that the add_filter
function is defined in the test variable scope, the WPLoader
module provided this, but the request triggered by the $I->amOnPage
method calls will be resolved in a separate PHP process: the test variable scope and the “site” variable scope, the one handling the $I->amOnPqge
requests, do not share any information or global values.
The solution
Remaining in the context of the possibilities offered by wp-browser, the way the test above could succeed is by including the WPFilesystem
module and leveraging its haveMuPlugin
method:
$I = new WpfunctionalTester($scenario);
$I->wantTo('verify the post title can be filtered');
$postId = $I->havePostInDatabase(['post_title' => 'Original title']);
$I->amOnPage("/index.php?p={$postId}");
$I->see('Original title');
$code = <<<PHP
add_filter('the_title', function(){
return 'Filtered title';
});
PHP;
$I->haveMuPlugin('title-filter.php', $code);
$I->amOnPage("/index.php?p={$postId}");
$I->see('Filtered title');
The WPFilesystem
module, a recent addition to the Codeception modules provided by wp-browser, will write the code needed to set up a must-use plugin in the WordPress installation folder; WordPress, in turn, will use the must-use plugin while handling the next requests.
The module will take care of removing any file it created after the tests, whether the result was a success or a failure.
A more realistic example
The example above, while explicative, seems hardly useful in “real life” coding so here it is a more complex, yet more realistic, example.
I might be developing a plugin that shows a cost for each post beside the title; the plugin allows developers to filter the cost string and currency symbol position.
While it makes sense to test this kind of code on an integration level, for the sake of this example, I will test it on an functional level:
$I = new WpfunctionalTester($scenario);
$I->wantTo('verify the cost string and currency symbol position can be filtered');
$fiveId = $I->havePostInDatabase([
'post_title' => 'Five',
'meta_input' => ['cost' => '5']
]);
$zeroId = $I->havePostInDatabase([
'post_title' => 'Zero',
'meta_input' => ['cost' => '0']
]);
// check default formatting
$I->amOnPage("/index.php/?p={$fiveId}");
$I->see('Five - $5');
$I->amOnPage("/index.php/?p={$zeroId}");
$I->see('Zero - $0');
$code = <<<PHP
add_filter('my_plugin_currency_position', function(){
return 'after';
});
// the plugin will provide filtering functions the complete cost
// string and the cost value used to generate it
add_filter('my_plugin_cost_string', function(\$cost_string, \$cost){
return \$cost == 0
? 'free!'
: \$cost_string;
},10,2);
PHP;
$I->haveMuPlugin('cost-filters.php', $code);
// check with filtering in place
$I->amOnPage("/index.php/?p={$fiveId}");
$I->see('Five - 5$');
$I->amOnPage("/index.php/?p={$zeroId}");
$I->see('Zero - free!');
Again: this might not be the best place to test the code in question still it provides one more way to do it and I’m sure I will come across edge cases that will require it.