Function Mocker 1.2.1 out!

A much needed refresh of the “necessary evil” library.

Monkey patching (thanks Patchwork)

The term “monkey patching”, when applied to code, refers to:

[…] changing code sneakily – and possibly incompatibly with other such patches – at runtime.

Source: WikiPedia.
The adverbs used in the sentence above, “sneakily” and “possibly incompatibly”, can be daunting.
But, exactly as mocking engines are based on the use of the dreaded PHP eval function for a good reason and a good cause, monkey patching should be regarded and treated in the same way.
The Patchwork library offers “userland” monkey patching for PHP code to allow redefining both internal and user defined functions and static methods.
The “userland” part refers to the fact that the library does not require the runkit extension to work.
While the library was born for testing purposes, and its API lends itself to it, I wanted to have something I could use in my WordPress tests with PhpUnit, Codeception, or wp-browser in a way similar to any other mocking engine.
So it was born, on the shoulders of Patchwork, function-mocker.

Unlawful mocking, stubbing and spying

Since I’ve just updated the library to support the latest possibilities offered by Patchwork, I thought it would be nice to show what it can do.
I’m assuming here a PhpUnit like test case containing test methods that could run in a vanilla PhpUnit setup, a Codeception, and a wp-browser powered one.
How could mocking internal PHP functions come in handy? A test is worth a thousand words:

<?php

// FunctionMocker needs the functions to be defined to replace them
function get_option($option)
{
    // no-op
}

function update_option($option, $value)
{
    // no-op
}

// The class under test
class Logger
{
    public function log($type, $message)
    {
        $option = get_option('log');
        $option[] = sprintf('[%s] %s - %s', date(DATE_ATOM, time()), $type, $message);
        update_option('log', sprintf('[%s] %s - %s', date(DATE_ATOM, time()), $type, $message));
    }
}

class InternaFunctionReplacementTest extends \PHPUnit\Framework\TestCase
{
    /**
     * It should log the correct message
     * @test
     */
    public function log_the_correct_message()
    {
        $mockTime = time();
        \tad\FunctionMocker\FunctionMocker::replace('time', $mockTime);
        \tad\FunctionMocker\FunctionMocker::replace('get_option', []);
        $update_option = \tad\FunctionMocker\FunctionMocker::replace('update_option');

        $logger = new Logger();

        $logger->log('error', 'There was an error');

        $expected = sprintf('[%s] error - There was an error', date(DATE_ATOM, $mockTime));
        $update_option->wasCalledWithOnce(['log', $expected]);
    }
}

There are three feats performed here by function-mocker:

  1. replacing the time internal function in the line

    $mockTime = time();
    \tad\FunctionMocker\FunctionMocker::replace('time', $mockTime);
    
    

    To allow for that, I’ve made the time function redefineable when initializing FunctionMocker in the tests bootstrap file:

    <?php
    require_once dirname(__DIR__) . '/vendor/autoload.php';
    
    \tad\FunctionMocker\FunctionMocker::init([
       'redefinable-internals' => ['time']
    ]);
    
    
  2. replacing the get_option function to make it return a value:

    \tad\FunctionMocker\FunctionMocker::replace('get_option', []);
    
    
  3. replacing and spying the update_option function:

    $update_option = \tad\FunctionMocker\FunctionMocker::replace('update_option');
    
    [...]
    
    $update_option->wasCalledWithOnce(['log', $expected]);
    
    

Read more on function-mocker repository.

With great power comes great responsibility

Whether I’m quoting Voltaire or Spider-Man it’s not certain; what’s sure is that Function Mocker, and mocking functions in general, has its uses and its pitfalls.
Stepping into the same terrain of the “eval is evil” meme, I’m listing some of the pitfalls I can think about:

  • untestable code makes a good case for refactoring; being able to test almost everything solves the problem. In the short term.
  • function and static method mocking allows mocking code that’s not under the control of the developer: the effort is usually best spent trying to develop good adapters and code with more choke points and control zones.
  • untestable code is a sign of code smell; painting it gold does not solve the underlying problem.

Words of caution aside, some testing is better than no testing.