12.5. WordPress Advanced Unit Tests

In the previous blog we saw tests that uses WordPress API functions to investigate the outcome of a method. However, such WordPress functions which are helpful to write tests are few, and to test majority of plugin methods, we need to rely on WordPress global variables and classes. Many WordPress functions store information in global variables or in classes which is used later to construct the output i.e. response sent to the browser. In unit tests, it is easy to fetch the data from the global variables or classes for tests.

In this blog, we explain test methods which rely on WordPress globals and classes, and also, show an easy way to find out the global variables used by such WordPress functions.

But before that, we touch upon another category of tests that simulate WordPress admin screen.

Admin Screen

The main file of the plugin, share-on-social.php, is coded in such way that when plugin is loaded, it includes front end php files. However, for admin screen along with front end files, it also includes admin php file. As front end files are loaded both for site screens as well for admin screen, we can test their inclusion easily. But, we need to simulate admin screen to test admin files inclusion.

WordPress uses class WP_Screen to define the screen and a global variable current_screen holds the current WP_Screen object. Normally, when a test runs the current_screen is null which indicates that tests are are executed in normal site screens. To switch to Admin screen, we have to create a new WP_Screen object and assign it to current_screen.

The trimmed version of Test_Share_On_Social::test_setup_for_admin() shows how it is done.

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

    public function test_setup_for_admin () {
        global $current_screen;
        $this->assertNull( $current_screen );

        $included_files = get_included_files();
                
        $file = SOS_PATH . 'admin/class-admin.php';
        $this->assertNotContains( $file, $included_files );
        
        $this->assertFalse( has_action( 'plugins_loaded', 'load_sos_admin' ) );
                
        // setup admin screen to make is_admin() as true

        
        $screen = WP_Screen::get( 'admin_init' );
        $current_screen = $screen;
        
        // now call setup
        setup_sos_plugin();
        
        // test for admin stuff
        $included_files = get_included_files();
        
        $file = SOS_PATH . 'admin/class-admin.php';
        $this->assertContains( $file, $included_files );
       
        $this->assertTrue( 
                has_action( 'plugins_loaded', 'load_sos_admin' ) == true );
       
        // revert back the screen
        $current_screen = null;
    }

To start with, we assert that global current_screen is null which confirms that we are in site screen. Next, with PHP function get_included_files(), we get list of loaded files and assert that admin files are not loaded as we are in site screen at this stage.

Next, we obtain an instance of admin screen with the static method WP_Screen::get(). The parameter admin_init is the hook name to which the screen is related. The returned screen object is assigned to global current_screen and with this, we are now effectively in admin screen and the WordPress function is_admin() function returns true.

Next, we call setup_sos_plugin() and as is_admin() returns true, it loads admin files in addition to the front end files. We test whether admin files are included and also, whether function load_sos_admin() is hooked to action plugin_loaded. Once tests are over, we revert back to site screen by assigning the current_screen to null.

We use WP_Screen also in some test methods in Test_Help and Test_Sos test cases.

 
 

WordPress Global Variables and Classes.

More often than not, WordPress functions use variety of global arrays to hold data of menu items, actions, scripts etc., which are subsequently used to build the response that is sent to the browser. Similarly, some functions use class objects such as WP_Screen, WP_Scripts etc., to hold data which again are accessed through global variables. Methods which use such functions can be tested by accessing the global arrays and class objects.

Test Localized Scripts

In Sos_Frontend::add_sos_script(), apart from enqueue, we also localize script data with the help of WordPress function wp_localize_script(). WordPress sends the localized data to the browser which is used by the scripts on client side.

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

    function add_sos_scripts () {
        ....            
        wp_localize_script( 'sos_script', 'sos_data', 
                    array(
                            'ajax_url' => admin_url( 'admin-ajax.php' ),
                            'nonce' => wp_create_nonce( 'sos-save-stat' ),
                            'gplus_client_id' => $gplus_client_id
                    ) );
    }

Since WordPress doesn’t provide any function to access the localized data, we test them using WordPress class WP_Scripts.

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

    public function test_localized_script () {
        global $wp_scripts;      
        ....
        // add shortcode and localize data
        $this->frontend->setup();
        $this->frontend->add_sos_scripts();
        
        $data = $wp_scripts->get_data( 'sos_script', 'data' );
        $data = substr( $data, strpos( $data, '{' ) - 1, strpos( $data, '}' ) + 1 );
        $sos_data = json_decode( $data, true );
        $this->assertCount( 3, $sos_data );        
        $this->assertarrayHasKey( 'ajax_url', $sos_data );
        $this->assertarrayHasKey( 'nonce', $sos_data );
        $this->assertarrayHasKey( 'gplus_client_id', $sos_data );
        $this->assertSame( $sos_data<a href="http://codex.wordpress.org/Function_Reference/wp_verify_nonce" target="_blank"> 'ajax_url' ],
                      'http://localhost/wp-admin/admin-ajax.php' );      
        $this->assertSame( $sos_data[ 'gplus_client_id' ], 'test_gplusclientid' );
        // wp_verify_nonce returns 1 or 2 when valid else false
        $this->assertSame( 1,wp_verify_nonce( $sos_data[ 'nonce' ], 'sos-save-stat' ) );
    }

WP_Script::get_data() returns the localized data as JSON string and PHP function json_decode() is used to decode which returns the array of localized data. Out of the localized data, nonce is a special case as its value is random. WordPress function [wp_verify_nonce() is used to test whether nonce is valid.

Test Custom Post Type

The method Sos::create_post_type() defined in share-on-social/admin/class-sos.php registers a custom post type and the test uses global array $wp_post_types to test it.

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

    public function test_create_post_type () {
        global $wp_post_types;
        
        if ( isset( $wp_post_types[ 'sos' ] ) ) {
            unset( $wp_post_types[ 'sos' ] );
        }
        $this->assertarrayNotHasKey( 'sos', $wp_post_types );
        
        // register post type
        $this->sos->create_post_type();
        $this->assertarrayHasKey( 'sos', $wp_post_types );
        
        $sos_type = $wp_post_types[ 'sos' ];
        $this->assertFalse( $sos_type->public );
        $this->assertFalse( $sos_type->hierarchical );
        $this->assertTrue( $sos_type->exclude_from_search );
        $this->assertFalse( $sos_type->publicly_queryable );
        $this->assertTrue( $sos_type->has_archive );
        
        $this->assertSame( 15, $sos_type->menu_position );
        $this->assertTrue( $sos_type->show_ui );
        $this->assertTrue( $sos_type->show_in_menu );
        $this->assertTrue( $sos_type->show_in_admin_bar );
        $this->assertFalse( $sos_type->show_in_nav_menus );
        $this->assertSame( SOS_URL . '/images/sos-icon.png', 
                $sos_type->menu_icon );
    }

Test Menu and Sub Menu

Method Sos_Options::register_settings_page() creates sub menu for the options page and WordPress holds sub menu data in global array $submenu.

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

    public function test_register_settings_page () {
        global $submenu;
        
        $this->assertFalse( isset( $submenu[ 'edit.php?post_type=sos' ] ) );
        $this->assertFalse( 
                Util::has_action( 'admin_page_sos_settings_page', 
                        $this->sos_options, 'render_settings_page' ) );
        
        $this->sos_options->register_settings_page();
        
        $this->assertTrue( isset( $submenu[ 'edit.php?post_type=sos' ] ) );
        $settings = $submenu[ 'edit.php?post_type=sos' ];
        $this->assertSame( 'Settings', $settings[ 0 ][ 0 ] );
        $this->assertSame( 'administrator', $settings[ 0 ][ 1 ] );
        $this->assertSame( 'sos_settings_page', $settings[ 0 ][ 2 ] );
        $this->assertSame( 'Common Options', $settings[ 0 ][ 3 ] );
        $this->assertTrue( 
                Util::has_action( 'admin_page_sos_settings_page', 
                        $this->sos_options, 'render_settings_page' ) );
    }

Throughout the Share on Social Plugin test suit you will find many such tests that uses WordPress global arrays and class objects.

 
 

Find Global Variable and Class

When you write tests extensively then it is important to learn a technique to find out whether a WordPress function uses global array or class object. Let’s explain this with an example.

Method Sos_Options::register_settings_page() uses WordPress function add_submenu_page(). To know, whether this function uses any global array, open the function reference page http://codex.wordpress.org/Function_Reference/add_submenu_page and scroll down to Source File section where you will find a link to the source file of the function.

WordPress Advanced Unit Tests - function source file

Open the source file and search for function add_submenu_page() and there you gather that it uses global $submenu and $menu among other globals. When function uses multiple globals, you may have to study the source little bit to understand which of them holds the functions' outcome.

Once you know the target global variable, you can echo it in the test method to know more about data held by it. As globals are typically array or class object. use print_r() function to output the data.

Bear in mind, some of these arrays and objects are huge in size. It is much easier to analyze the output if data is piped to some file.

In the next blog, we explain the unit tests for internationalization functions.