Better functional tests for WordPress in Codeception – 01
May 17, 2017
Using WordPress code in functional tests.
User voice
Not a long time ago I was lamenting the limited implementation of the WordPress
module part of wp-browser.
The inability to tap into WordPress methods directly was a limitation that made testing some WordPress implementations, on a functional level, impossible.
Way before I did, other wp-browser users had noticed and reported the limitation; sometimes with good reasons and arguments and sometimes due to a misunderstanding of how "stuff" should be tested at what level.
Well, the first step in that direction is done with the release of version 1.20
of wp-browser.
A code example
In this example I want to test if, as a WordPress site administrator, I could create posts in the site using the WordPress REST API and its content endpoints.
The functional test I'd write could be this:
/**
* It should allow an admin to insert a post
*
* @test
*/
public function it_should_allow_an_admin_to_insert_a_post(FunctionalTester $I) {
$I->sendPOST( 'wp-json/wp/v2/posts/', [
'title' => 'A post',
'content' => 'A post content',
] );
$I->seeResponseCodeIs( 201 ); // it means "created"
$I->seeResponseIsJson();
}
The problem with this test is that the REST API has no way to identify the posting user and will, thus, negate the request and return a 403
(unauthorized) status.
The way the REST API identifies a user is by verifying a nonce created for the current user and the wp_rest
action.
The function to create a nonce is wp_create_nonce()
, but that function is not available in the scope of the functional test in question.
The latest version of the WPLoader module introduces an option to "just bootstrap WordPress" (understanding the consequences of doing it) in the tests' variable scope.
With reference to wp-browser README I can configure my suite to use the modules I need for the tests:
class_name: FunctionalTester
modules:
enabled:
- \Helper\Functional
- REST
- WPDb
- WPBrowser
- Asserts
- WPLoader
config:
# needed by the REST module
WPBrowser:
url: 'http://wp.localhost'
adminUsername: admin
adminPassword: admin
adminUrl: /wp-admin
# to test the REST API
REST:
depends: WPBrowser
url: 'http://wp.localhost'
# to manage database insertions and states
WPDb:
dsn: 'mysql:host=localhost;dbname=wp'
user: root
password: root
dump: tests/_data/dump.sql # load this...
populate: true # ...before the tests...
cleanup: true # ... and between tests
url: 'http://wp.localhost'
tablePrefix: wp_
# to get access to WordPress code in the tests
WPLoader:
# just load WordPress using the same db as WPDb
loadOnly: true
wpRootFolder: /tmp/wordpress
dbName: wp
dbHost: localhost
dbUser: root
dbPassword: root
After a run of codecept build
I can update the test to use, and now I can, WordPress functions to create the nonce I need.
/**
* It should allow an admin to insert a post
*
* @test
*/
public function it_should_allow_an_admin_to_insert_a_post(FunctionalTester $I) {
// create a user that can create posts
$user_id = $I->haveUserInDatabase( 'user', 'administrator', [ 'user_pass' => 'user' ] );
// login to get the cookies set
$I->loginAs( 'user', 'user' );
// gather the nonce "recipes"
$_COOKIE[ LOGGED_IN_COOKIE ] = $I->grabCookie( LOGGED_IN_COOKIE );
wp_set_current_user( $user_id );
// create the nonce
$nonce = wp_create_nonce( 'wp_rest' );
// this is the header the REST API will look up to find the nonce
// and identify the user together with the cookies
$I->haveHttpHeader( 'X-WP-Nonce', $nonce );
$I->sendPOST( 'http://tribe.localhost/wp-json/wp/v2/posts/', [
'title' => 'A post',
'content' => 'A post content',
] );
$I->seeResponseCodeIs( 201 );
$I->seeResponseIsJson();
}
To avoid repetition, the first part of the test can be moved in a Codeception Step object and reduce the test code even more:
/**
* It should allow an admin to insert a post
*
* @test
*/
public function it_should_allow_an_admin_to_insert_a_post(FunctionalTester $I) {
$I->setupAuthForA('administrator'); // we do not really care about name, password et cetera
$I->sendPOST( 'http://tribe.localhost/wp-json/wp/v2/posts/', [
'title' => 'A post',
'content' => 'A post content',
] );
$I->seeResponseCodeIs( 201 );
$I->seeResponseIsJson();
}
Next
This new development opens some previously closed doors but there is more in the new version of the WPLoader
module I will explore in a next post.