skip to Main Content

I am building a simple WordPress plugin and I am looking to add unit tests using PHPUnit, I have the following class code:

<?php

namespace App;

class MyPlugin {

    public function __construct()
    {
        add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']);
        add_action('admin_menu', [$this, 'createAdminMenu']);
        add_action('admin_init', [$this, 'settings']);
    }

    public function enqueueAdminScripts()
    {
        // Code here
    }

    public function createAdminMenu()
    {
        // Code here
    }

    public function settings()
    {
        // Code here
    }

}

And this is my test file:

<?php

use AppMyPlugin;
use PHPUnitFrameworkTestCase;

class MyPlugin extends TestCase {

    protected $myPlugin;

    protected function setUp(): void
    {
        $this->myPlugin = new MyPlugin();
    }

    public function testRegister()
    {
        $this->assertInstanceOf('AppMyPlugin', $this->myPlugin);
    }

}

However I am always getting the error Error: Call to undefined function Appadd_action()

What is the best way of mocking the add_action calls purely within PHPUnit without having to rely on a third party solution?

Any help with this would be greatly appreciated.

2

Answers


  1. Below is a very simple mock of the hook system. It doesn’t take into account priorities, and it is more modernized for expected usage so it might not support some previous edge cases.

    You’ll notice that add_action actually calls the exact same code as add_filter, and that’s what core actually does.

    The gist is that there’s a global array. You could store this in your base class if you wanted, too. WordPress backs their stuff with objects, too, but I’m simplifying things here.

    global $wp_test_hooks;
    
    if(!is_array($wp_test_hooks)) {
        $wp_test_hooks = [];
    }
    
    if(!function_exists('add_filter')) {
        function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
            global $wp_test_hooks;
            
            $wp_test_hooks[$hook_name][] = [
                $callback,
                $priority,
                $accepted_args,
            ];
            
            return true;
        }
    }
    
    if(!function_exists('add_action')) {
        function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
            return add_filter( $hook_name, $callback, $priority, $accepted_args );
        }
    }
    
    if(!function_exists('apply_filters')){
        function apply_filters( $hook_name, ...$values ) {
            $value = array_shift($values);
            global $wp_test_hooks;
            if(array_key_exists($hook_name, $wp_test_hooks)){
                foreach($wp_test_hooks[$hook_name] as $parts){
                    list($callback, $priority, $accepted_args) = $parts;
                    $value = $callback($value, ...$values);
                }
            }
            
            return $value;
        }
    }
    
    if(!function_exists('do_action')){
        function do_action( $hook_name, ...$arg ) {
            global $wp_test_hooks;
            if(array_key_exists($hook_name, $wp_test_hooks)){
                foreach($wp_test_hooks[$hook_name] as $parts){
                    list($callback, $priority, $accepted_args) = $parts;
                    $callback(...$arg);
                }
            }
        }
    }
    

    You can see it in action by using normal WordPress code:

    add_action('name1', static function(){echo 'In the Action';});
    add_filter('name2', static function($originalValue){return 'In the Filter';});
    
    do_action('name1', 'test', 'stuff');
    echo PHP_EOL;
    echo apply_filters('name2', 'test', 'stuff');
    

    Demo here: https://3v4l.org/DAVl0

    All that said, if you don’t manually call do_action or apply_filters in your code, don’t both with the bulk of this. You can use the functions, just leave their bodies empty. More often than not, it is WordPress or a plugin that is invoking those methods, and if you aren’t invoking WordPress or calling those functions, there’s no need for logic or even the global array. What you probably want is:

    if(!function_exists('add_filter')) {
        function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { }
    }
    
    if(!function_exists('add_action')) {
        function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { }
    }
    
    if(!function_exists('apply_filters')){
        function apply_filters( $hook_name, ...$values ) { }
    }
    
    if(!function_exists('do_action')){
        function do_action( $hook_name, ...$arg ) { }
    }
    
    Login or Signup to reply.
  2. I’d say this depends a lot what you’d actually want to test (and therefore call).

    Reading your question you would like to put WordPress itself out of the equation to essentially test your code.

    That is testing a plugin not as an integration test.

    Now WordPress does not make it exactly easy to test but you could try to get as far as possible. I’d say the issue you describe in the question lays in the constructor:

        public function __construct()
        {
            add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']);
            add_action('admin_menu', [$this, 'createAdminMenu']);
            add_action('admin_init', [$this, 'settings']);
        }
    

    This constructor only works within WordPress. But now you want to test it without. Well, easy:

        public function __construct()
        {
        }
    

    Problem sovled. Now you’d say, this is overly clever, right? The plugin needs to be initialized. True:

        public static function setup(): MyPlugin
        {
            $plugin = new self();
            add_action('admin_enqueue_scripts', [$plugin, 'enqueueAdminScripts']);
            add_action('admin_menu', [$plugin, 'createAdminMenu']);
            add_action('admin_init', [$plugin, 'settings']);
    
            return $plugin;
        }
    

    With this, you still have one central way to setup your plugin while you’re able in your unit-tests to test calling the concrete functions. The new operator (in your test) and the way you’ve written the constructor does not stand in your way any longer and the code became testable.

    If those as well depend on WordPress functions, you’ll run into the same problem thought. However the functionality you actually may want to test here might not entirely, so extracting the code that does not into methods of its own, allow actual unit-tests of those independent methods (those that have no side-effects on the global scope which WordPress uses).

    And I’d say your nose is good here: At the end of the day you want as many of these pure methods (without side-effects) and less those that depend on global state and WordPress as a dependency.

    You could do other tests (e.g. integrating with WordPress) with the WordPress base test case class to not reinvent the wheel. To only do that for certain methods, group your tests in different directories, e.g. test/unit and test/wordpress.

    Then you can decide what kind of test you want to write.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search