The path to real WordPress functional testing – 02

The first roadblocks and the first workarounds.

First tests, first problems

As soon as I got the idea of trying again to work on a real functional testing solution for wp-browser a list of things that blocked me the previous time, and that led to the current incarnation of the WordPress module, jumped to my mind.
First among those issue the inner working of the load_template function that is the cornerstone, in turn, of the get_header, get_footer and get_template_part functions.
The first two test methods I wrote are these:

use WordPressfunctionalTester as Tester; class BasicNavigationCest { public function _before(Tester $I) { $I->useTheme('twentyseventeen'); } /** * It should allow navigating to the site homepage * * @test */ public function should_allow_navigating_to_the_site_homepage(Tester $I) { $I->amOnPage('/'); $I->seeElement('body.home'); } /** * It should allow navigating to the site homepage multiple times * * @test */ public function should_allow_navigating_to_the_site_homepage_multiple_times(Tester $I) { $I->amOnPage('/'); $I->seeElement('body.home'); $I->amOnPage('/'); $I->seeElement('body.home'); $I->amOnPage('/'); $I->seeElement('body.home'); } } 

Passing the first test method is easy: just include the site /index.php file.
Passing the second, without getting a fatal… not that easy due to the load_template limit.

Once and only once

The load_template function has the following code:

// file wp-includes/template.php function load_template( $_template_file, $require_once = true ) { global $posts, $post, $wp_did_header, $wp_query, $wp_rewrite, $wpdb, $wp_version, $wp, $id, $comment, $user_ID; if ( is_array( $wp_query->query_vars ) ) { extract( $wp_query->query_vars, EXTR_SKIP ); } if ( isset( $s ) ) { $s = esc_attr( $s ); } if ( $require_once ) { require_once( $_template_file ); } else { require( $_template_file ); } } 

By default the $require_once parameter is set to true and thus, when not overridden, template files will only be included once using the require_once instruction.
The function is called, without overriding that $require_once parameter, in the get_header(), get_footer() and get_template_part() functions and those, in turn, are the foundation of any WordPress theme.
The reason the second test method will fail is that the second and third calls to $I->amOnPage('/') will not trigger a new inclusion of the theme template files and no content will be output.
To get around this issue I’ve relied on the trusted Patchwork library to make the the locate_template function work as intended.
The end result is that the tests above will pass:

Down first issue.

Can I hook?

The fundamental advantage “real” functional testing offers is the possibility to hook into actions and filters in the test code and see the hooked functions called while handling the request; to put the sentence in test code I wrote the test code below:

<?php use WordPressfunctionalTester as Tester; class ActionsCest { public function _before(Tester $I) { $I->useTheme('twentyseventeen'); } /** * It should allow hooking on an action while navigating to the homepage * * @test */ public function should_allow_hooking_on_an_action_while_navigating_to_the_homepage(Tester $I) { $wp_headFired = false; $wp_footerFired = false; add_action('wp_head', function () use (&$wp_headFired) { $wp_headFired = true; echo '<meta foo="bar">'; }); add_action('wp_footer', function () use (&$wp_footerFired) { $wp_footerFired = true; echo '<p>Hello from the footer</p>'; }); $I->amOnPage('/'); $I->assertTrue($wp_headFired); $I->seeInSource('<meta foo="bar">'); $I->seeInSource('<p>Hello from the footer</p>'); } /** * It should allow visiting the homepage multiple times * * Testing that the `require_once` of locate template is eluded... * * @test */ public function should_allow_visiting_the_homepage_multiple_times(Tester $I) { $wp_headFiredTimes = 0; $wp_footerFiredTimes = 0; add_action('wp_head', function () use (&$wp_headFiredTimes) { ++$wp_headFiredTimes; }); add_action('wp_footer', function () use (&$wp_footerFiredTimes) { ++$wp_footerFiredTimes; }); $I->amOnPage('/'); $I->amOnPage('/'); $I->amOnPage('/'); $I->assertEquals(3, $wp_headFiredTimes); $I->assertEquals(3, $wp_footerFiredTimes); } } 

In the first case I’m making sure functions hooked to WordPress filters in the tests scope, to the wp_head and wp_footer actions in this case, will be called and will be able to output content; in the second test I want to make sure that, whatever solution I come up with, it will really include the file triggering the actions, the theme index.php file in this case, each time.
If the solution I put in place was based, as an example, on output buffering and caching, the first test would pass but the second would fail.
Some time later I was able to make both test pass relying again on Patcwork:

Still, there are limits

As much as I’d like to say “anything can be done”, that’s not the case.
To try and see how far I can push this solution I wrote this test:

use WordPressfunctionalTester as Tester; class LoginCest { /** * It should allow logging in and have the request use the authenticated user * * @test */ public function should_allow_logging_in_and_have_the_request_use_the_authenticated_user(Tester $I) { add_action('wp_footer', function () { if (is_user_logged_in()) { echo 'User id is ' . get_current_user_id(); } else { echo 'User is not logged in'; } }); $userId = $I->haveUserInDatabase('user', 'subscriber', ['user_pass' => 'password']); $I->loginAs('user', 'password'); $I->amOnPage('/'); $I->see("User id is {$userId}"); $I->dontSee('User is not logged in'); } } 

Now: including a theme index file a couple of times seemed like a walk in the park; simply put, due to redefined functions, constants and pervasive use of die and exit, including any “real” WordPress file like the wp-login.php one, or all the files in the /wp-admin folder, is not possible.
Due to how I’ve previously implemented the WordPress module, making each request in a separate PHP process, I can “get around” the issue doing some requests in a separate PHP process and some including the file in the same scope as the tests; the former will be done for any risky file, the second for all “safe” files.
In the test method the loginAs() request is handled in a process, while the amOnPage one is handled in the same scope of the tests; this solution gets the job done:

The fact that the login request happens in a separate process means that any filtering applied to hooks fired during the user authentication will not apply; the test below will, in fact, fail:

/** * It should fire hooks during the login process * * @test */ public function should_fire_hooks_during_the_login_process(Tester $I) { $fired = false; add_action('login_head', function () use (&$fired) { $fired = true; }); $I->loginAsAdmin(); $I->assertTrue($fired); } 

So far this is ok with me: I’m closer to real functional testing then I was before and there is a workable base for authentication too.


I will try to map out all the shortcomings of this solution while using it to test my Action-Domain-Responder implementation.

I appreciate your input