Advanced Testing with PHPUnit in WordPress

Introduction

Once you’ve gotten a few basic tests written—inevitably—you’ll you’ll find yourself needing to move beyond the basics to test larger and more complex code bases. In this lesson we’ll briefly cover:

Helper classes

Working withing the WP_UnitTestCase framework, it can be easy to forget that it’s just code. This means we have all of the regular tools and tricks available to us. Consider Using:

Non-Test Methods

This one’s painfully obvious once it’s pointed out—You don’t have to write methods that start with test_. Feel free to include as many helper methods (both instance and static) that are called by your test_ methods—Helper methods can even include assertions!

public function validateFormEnqueued(){
cx_enqueue_form_processor();
global $wp_scripts;
$this->assertContains($this->script_handle, $wp_scripts->queue);
$this->assertContains("{$this->script_handle}-helpers", $wp_scripts->queue);

$inline=$wp_scripts->registered[$this->script_handle]->extra['before'][1];
$this->assertNotFalse(strpos($inline, 'const cxNext'));
}

Leveraging OOP for Test Purposes

This one can also be a bit of a ‘face-palm’ moment when you realize it: You can use any OOP patterns you may want to apply for your test cases. Remember, your phpunit.xml file tells phpunit where to run tests and your bootstrap.php file indicates what to pre-load. But your test files themselves are free to require additional files as needed. A good “warm up” is to set up a testing trait that you can easily re-use across multiple test classes.

trait BrandFactory{
    protected static $post_id=false;
    public $agreement_id;

    public function makeBrand(){...}

    public function resetBrand(){...}

}
class TestEnqueueFormProcessor extends \WP_UnitTestCase{
    use \CX\Tests\BrandFactory;

    public $code='teb';
    public $slug='test-enqueue-brand';
    public $script_handle='cx-form-processor';

    public function setUp(){...}

    public function test_enqueueValidBrandPage(){...}

    public function test_notEnqueuedOnOtherPages(){...}

    public function validateFormEnqueued(){...}
}

Adding Composer Packages

For those who use the Composer pacakge manager, you’ll find a wide world of helper libraries out there to extend PHPUnit testing. To make it easy to extend your tests, your docker image comes preloaded with composeryou know CWPDEV loves ya 😉. you can install and test in the /tmp directoryIt’ll be autoloaded into your test environment from here. Once you’re satisfied you want to use it, add it to your DockerfileLine 27, as of this writing and rebuild the image to make it a permanent addition to your test setup.

#wpphptests
FROM php:7.4.25-cli-bullseye
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions bcmath xdebug mysqli exif gd imagick zip


RUN apt-get update
RUN apt-get -y install subversion git mariadb-server

COPY install-wp-tests.sh /install-wp-tests.sh
RUN service mariadb start && mysqladmin -u root password 'root' && /install-wp-tests.sh

WORKDIR "/tmp"
RUN curl -LO https://phar.phpunit.de/phpunit-7.5.20.phar && mv phpunit-7.5.20.phar /usr/local/bin/phpunit
RUN chmod +x /usr/local/bin/phpunit

#composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
RUN php -r "if (hash_file('sha384', 'composer-setup.php') === '906a84df04cea2aa72f40b5f787e49f22d4c2f19492ac310e8cba5b96ac8b64115ac402c8cd292b8a03482574915d1a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
RUN php composer-setup.php
RUN php -r "unlink('composer-setup.php');"
RUN mv composer.phar /usr/local/bin/composer
RUN composer require --dev yoast/phpunit-polyfills

RUN echo "require_once('/tmp/vendor/autoload.php');">> /tmp/wordpress-tests-lib/wp-tests-config.php
#ATTENTION: Add your composer packages here!!
WORKDIR "/root"
#wp-cli for pleasant user experience
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
	chmod +x wp-cli.phar && \
	mv wp-cli.phar /usr/local/bin/wp
RUN wp core download --force --allow-root
RUN service mariadb start && \
	wp core config --dbname=wp_tests --dbuser=root --dbpass=root --dbhost=127.0.0.1 --dbprefix=wp_ --allow-root &&\
	wp core install --url=http://localhost:8080 --title=Test --admin_user=admin --admin_email=root@email.com --allow-root

RUN echo 'alias wp="wp --allow-root"'>> /root/.bashrc
RUN echo "xdebug.mode=coverage">> /usr/local/etc/php/php.ini-development && \
	cp /usr/local/etc/php/php.ini-development  /usr/local/etc/php/php.ini

WORKDIR "/root/wp-content"
ENTRYPOINT service mariadb start && bash

Test Doubles

Occasionally we need an object/class/component to act in a prescribed manner in our tests, rather than operating as it would “out in the wild.” In Unit Testing this accomplished through the use of stubsWhich return a predefined value when a method is called and mockswhich listen for calls to their methods and report the occurence. PHPUnit comes with good support for OOP mocking and stubbing and there are various composer packages that enhance this even further.

But, as WordPress devs. We have a somewhat unique challenge in that their are A LOT of global variables, and functions that cannot be mocked with basic tools. So we’ll discuss two alternate approaches: and use namespace overloading to get the job done.

Example: Mocking with Namespaces

Namespace overloading takes advantage of PHP’s protocol for resolving methods in a namespace. It requires that we use a namespace in our custom codeI’ll confess to being a late adopter of this practice—but it really does just make life easier across the entire coding spectrum., and that we do NOT fully qualify our global methods.e.g. use get_post(), not \get_post()

Then, in our test class file we use the same namespace and define the function we want to use in our test, in that namespace. This is a simple example—but you get the idea. Once you’ve overridden the global method, you have complete control.Keep in mind you can only get away with overloading a global variable once per test battery

<?php
namespace CWPDEV/CustomPlugin;

function createAPINonce(){
    return wp_create_nonce('special_action');
}
<?php
namespace CWPDEV/CustomPlugin;

function wp_create_nonce($action=-1){
    if('special_action' === $action){
        return '12345';
    } else {
        return 'abcde';
    }
}
class testAPINonce extends \WP_UnitTestCase {
    function test_createNonce(){
        $value='12345';
        $this->assertNotEquals($value, wp_create_nonce());
        $this->assertEquals($value, createAPINonce());
    }
}

WP_Mock

I’ll admit, I haven’t had a chance to play with this library much—but it looks to be everything you could want and more. It comes included on the testing container—enjoy!

<?php
/*The autoloader is already included, so you just need to boostrap the library at the end of the boostap.php file*/
// Start up the WP testing environment.
require "{$_tests_dir}/includes/bootstrap.php";

WP_Mock::bootstrap();
<?php
/*Your classes will should extend the WP_Mock test case to get full usage*/
class TestMappingConnection extends \WP_Mock\Tools\TestCase{
...
}

Conclusion

With that, you’ve got a good set of initial tools and tricks to start your automated testing journey with your PHP code. But how can we measure the effectiveness of our tests?

Well, we’ll talk about that next. 🙂

×

Keyboard shortcuts

CTRL+Shift+F Search slideshow
F Fullscreen view
CTRL+Click Zoom in
Esc Topic overview
Right arrow,
Down arrow
Next slide
Left arrow,
Up arrow
Previous slide

Color codes

Tooltip
Hover over text more additional info
Link to an external resource
Link to an internal slide
If buttons aren't working, click in the screen to "focus" your browser
RSS
LinkedIn
Share