12.4. Unit Test Scripts, Actions, Filters

In the previous blog, we saw how to write tests for plugin methods that returns value or echo output. In this blog, we explore third category of plugin methods where outcome is tested using corresponding WordPress API functions.

For example, add_action() which registers an action hook is tested using WordPress function has_action().

But, before exploring such tests, we briefly explain an utility class used by Share on Social Plugin test cases to reduce boilerplate code in tests and also, learn to toggle user capability to test methods which depends on user capability.

Utility Methods for Tests

To investigate the arrays, fetch object from database and change user role or capability etc., tests require multiple lines of code. The class Util defined by share-on-social/tests/phpunit/util/class-util.php, extracts such redundant boilerplate code into utility methods.

Methods to investigate the multidimensional arrays and check whether action exists.

  • has_value() - whether multidimensional array contains a value.

  • has_obj() - whether multidimensional array contains an object of a class.

  • has_action() - wrapper for WordPress has_action() function.

  • has_filter() - wrapper for WordPress has_filter() function.

  • get_action() - returns action array from global array.

Methods to fetch posts and change locale.

  • get_post_id() - get post id for matching meta key and value.

  • count_posts() - get number of posts of a post type.

  • change_locale() - changes locale for the tests.

Toggle Capability

Util class also defines methods to manage users and capabilities within tests.

  • set_cap() - enable or disable a capability.

  • set_activate_plugins_cap() - enable or disable activate_plugins capability.

  • set_admin_role() - enable or disable admin role to an user.

In many of the tests, setUp() method uses following constructs to set the user and capability to run the tests.

share-on-social/tests/phpunit/tests/test-options.php

class Test_Sos_Options extends WP_UnitTestCase {

    public function setup () {
        parent::setup();  
        ....
        wp_set_current_user( 1 );
        Util::set_admin_role( true );
    }

Having gone through the utility class and its methods, let’s get back to the tests that uses WordPress API functions to assert the outcome of method under test.

 
 

Test Actions and Filters

In Share on Social Plugin classes we use setup() method to add actions and filters.

For example, Sos_Options::setup() method adds actions for admin_init and admin_menu hooks.

share-on-social/admin/class-options.php

class Sos_Options {

    public function setup () {
        add_action( 'admin_init', 
                array( $this,'init_common_options' ) );
        add_action( 'admin_menu', 
                array( $this,'register_settings_page' ) );
    }

We test actions in Test_Sos_Options using utility method Util::has_action().

share-on-social/tests/phpunit/tests/test-options.php

class Test_Sos_Options extends WP_UnitTestCase {

    public function test_setup () {
        $this->assertFalse( 
           Util::has_action( 'admin_init', $this->sos_options,'init_common_options' ) );
        $this->assertFalse( 
           Util::has_action( 'admin_menu', $this->sos_options,'register_settings_page' ) );
        
        $this->sos_options->setup();
        
        $this->assertTrue( 
           Util::has_action( 'admin_init', $this->sos_options, 'init_common_options' ) );
        $this->assertTrue( 
           Util::has_action( 'admin_menu', $this->sos_options, 'register_settings_page' ) );
    }

In Test_Sos_Options::test_setup(), we first test that admin_init and admin_menu hooks’ actions are not set. Next, we call method under test with $this->sos_options->setup(). Finally, we test whether admin_init has action pointing to Sos_Option::init_common_options() method. For this, we use Util::has_action() which is wrapper for WordPress function has_action(). Similarly we test admin_menu action.

Filters are tested, in same fashion, using Util::has_filter() method which is wrapper for WordPress function has_filter().

Demarcate Unit Tests

Is it necessary to test the outcome of action or filter. No, remember that we unit test the plugin and not the WordPress core. It is sufficient to test whether action is registered for the hook and thereafter, it is responsibility of WordPress core to handle the action.

Test Enqueue Scripts

Next type of tests are related to enqueue of plugin scripts and styles. Share on Social Plugin enqueues couple of scripts and a style sheet in Sos_Frontend::add_sos_scripts() method.

share-on-social/frontend/class-frontend.php

    function add_sos_scripts () {
        ....
        wp_register_style( 'sos_style', SOS_URL . '/css/style.css' );
        wp_register_script( 'sos_script', SOS_URL . '/js/share.js', 
                            array('jquery'), '', true );
     
        if ( has_shortcode( $post->post_content, $this->sos_shortcode ) ) {
            wp_enqueue_style( 'sos_style' );
            wp_enqueue_script( 'sos_script' );
            ....
        }
     }

The method, registers the script and stylesheets and if, share-on-social shortcode exists in the post, then it enqueue them. WordPress functions wp_script_is() and wp_style_is() are useful to check whether scripts and stylesheets are registered or enqueued.

share-on-social/tests/phpunit/tests/test-frontend.php

    public function test_add_sos_scripts () {
        // if no shortcode 
        $this->frontend->add_sos_scripts();
        
        $this->assertTrue( wp_style_is( 'sos_style', 'registered' ) );
        $this->assertTrue( wp_script_is( 'sos_script', 'registered' ) );
        
        $this->assertFalse( wp_style_is( 'sos_style', 'enqueued' ) );
        $this->assertFalse( wp_script_is( 'sos_script', 'enqueued' ) );
        
        // if shortcode exists 
        $this->frontend->setup();
        $this->frontend->add_sos_scripts();
        
        $this->assertTrue( wp_style_is( 'sos_style', 'enqueued' ) );
        $this->assertTrue( wp_script_is( 'sos_script', 'enqueued' ) );
    }

The test calls method under test Sos_Frontend::add_sos_scripts() and then, assert that script and stylesheet are registered but not enqueued. Next, it calls Sos_Frontend::setup() to register the shortcode and assert script and stylesheet are enqueued.

Sos_Frontend::add_sos_scripts() enqueues scripts only if post contains share-on-social shortcode. It is important to understand, from where it obtains the post, within tests, that has the shortcode. For that, we need to look at Test_Frontend::setUp()method where the global post is set.

share-on-social/tests/phpunit/tests/test-frontend.php

class Test_Frontend extends WP_UnitTestCase {
    ....
    public function setUp () {
        parent::setUp();
        
        global $post;
        $this->create_posts();
        $post = get_post( $this->child_post_id );
        setup_postdata( $post );
        ....
    }

    private function create_posts () {
        $args = array(
                'post_name' => 'parent test page',
                'post_title' => 'parent test page title',
                'post_status' => 'publish',
                'post_type' => 'page',
                'post_content' => '<a href="http://codex.wordpress.org/Function_Reference/setup_postdata" target="_blank">[share-on-social][/share-on-social]]'
        );
        $post_id = $this->factory->post->create( $args );
        $this->parent_post_id = $post_id;
        
        $args = array(
                'post_name' => 'child test page',
                'post_title' => 'child test page title',
                'post_status' => 'publish',
                'post_type' => 'page',
                'post_parent' => $this->parent_post_id,
                'post_content' => '[[share-on-social]test-content[/share-on-social]]'
        );
        $post_id = $this->factory->post->create( $args );
        $this->child_post_id = $post_id;
    }

In Test_Frontend::setUp() we call a private method Test_Frontend::create_posts() to create two posts - actually two pages and one of which is the parent of other. The args[‘post_content’] of these pages holds the share-on-social shortcode. We use factory method of WP_UnitTestCase to create the posts.

WordPress holds the current post in the global variable $post. We set the child page as current post by assigning it to the global $post and then, update other global variables related to current post, by calling WordPress function [setup_postdata() . In the test, when add_sos_scripts() is called, it obtains the current post from global $post and checks whether it has the shortcode or not and enqueue scripts accordingly.

 
 

Factory Methods

WordPress Test Library class WP_UnitTestCase comes with a set of factory methods to create WordPress objects such as Post, Attachment, Comment, User, Term, Category and Tag.

For example, in a test case, we use following construct to create a post object using factory method..

        $args = array(
                'post_name' => 'parent test page',
                'post_title' => 'parent test page title',
                'post_status' => 'publish',
                'post_type' => 'page',
                'post_content' => '[[share-on-social][/share-on-social]]'
        );
        $post_id = $this->factory->post->create( $args );

Similarly, comments can be created through $this->factory->comment->create( $args ) with appropriate values in args array.

Factory offers following methods for each type.

  • create_object ( $args ) - creates an object.

  • update_object ( $user_id, $fields ) - updates an object.

  • get_object_by_id ( $user_id ) - fetch an object.

Apart from these common methods, factory also has additional methods for some of the types. To know more about the factory, browse the source file share-on-social/tests/phpunit/includes/factory.php

The next blog explains the unit tests for methods which uses WordPress global variables and also, simulate WordPress admin screens to carry out tests related to admin modules.