Closure Binding for Unit testing of Private and Protected Methods

Some years ago, I wrote about using closure binding to access protected and private properties when unit testing an object, to verify internal state; and I created the SpyMaster library to simplify that task. One feature that I didn’t include in the version of SpyMaster that I released publicly on github was the ability to execute protected and private methods within an object.

I know that a lot of developers will declare internal methods as public if they want to make them testable; but defining methods that are supposed to be internal to a class as public is rarely a good idea, because it is then a part of the public API for the class, and can be accessed from external code.
Other developers might use Reflection in their unit tests to make private or protected methods executable. That’s a better approach, because the class under test retains its internal methods as protected or private.

An alternative approach is to use closure binding to execute those methods without needing to change their accessibility at all, similar to the way that the original SpyMaster library accesses internal properties without changing their visibility.

Let’s start by defining a simple class, with a private method that we want to test:


class Greeting {
    private string $value;

    public function __construct(string $value) {
        $this->value = $value;
    }

    private function show(?string $addition = null): string {
        return $this->value . ' ' . ($addition ?? "World");
    }
}

$sut = new Greeting('Hello');

So it’s a simple enough class for demonstration purposes, and we want to test the private show() method.

If we want to use reflection to test the show() method, we would code our test like:


$reflectionClass = new ReflectionClass($sut);
$reflectionMethod = $reflectionClass->getMethod('show');
$reflectionMethod->setAccessible(true);

and we can then test the method using:


$result = $reflectionMethod->invokeArgs($sut, ['World']);

or


$result = $reflectionMethod->invoke($sut, 'World');

Now let’s take a look at how a new variant of the SpyMaster class would work: the class itself doesn’t contain much actual code:


class SpyMaster {
    protected object $sut;

    public function __construct(object $sut) {
        $this->sut = $sut;
    }

    private function getAgent(): Callable {
        return function ($method, ...$args) {
            if (method_exists($this, $method) === false || is_callable([$this, $method]) === false) {
                throw new Exception(sprintf('Method %s does not exist in class %s', $method, self::class));
            }

            return $this->$method(...$args);
        };
    }

    public function __call(string $method, $args) {
        $executor = $this->getAgent()
            ->bindTo($this->sut, $this->sut::class);

        return $executor($method, ...$args);
    }
}

Note that I’m using $this->sut::class when I bind the closure to the test class, which requires PHP8: to use this with earlier versions of PHP, that should be changed to get_class($this->sut) instead, which will allow us to use it with PHP versions from 7.4.0 and above.

To use SpyMaster, we instantiate the SpyMaster class, passing in an instance of the class that we want to test as a constructor argument, and then call the method that we want to test in the Greeting class, in this case show().


$testGreeting = new SpyMaster($sut);

$result = $testGreeting->show('Mark');

If we want to use PHP8 named arguments, then:


$result = $testGreeting->show(addition: 'Mark');

will also work.

When we call the method under test against the SpyMaster instance, the method call is intercepted by the SpyMaster’s magic __call() method. This calls getAgent() to create the Closure, that we then bind to the instance of the object that we’re testing. Then we execute the Closure, passing in the name of the method that we want to call, with any arguments that we need to pass.
The closure itself tests whether the method exists and is callable (in the scope of the $this, which is the object that it’s bound to, the instance of Greeting), throwing an Exception if not; otherwise it calls the method in the bound object, passing in any arguments, and returns the result.


There are limitations with this SpyMaster code. As with using Reflection, it won’t support pass-by-reference arguments. And the code that I’ve shown above won’t allow execution of private methods in a parent class if the class that we’re testing uses inheritance. The latter can be supported, although it does add a degree more complexity to the SpyMaster code.

Let’s start by extending our Greeting class:


class ExtendedGreeting extends Greeting {
    public function __construct(string $value) {
        parent::__construct($value);
    }
}

$sut = new ExtendedGreeting('Hello');

Now we need to modify SpyMaster so that we can specify that the method we want to test is in the Greeting class, rather than in ExtendedGreeting, and we’ll need to bind the Closure at that level in the inheritance hierarchy.


class SpyMaster {
    protected object $sut;
    protected ?string $inheritanceLevel;

    public function __construct(object $sut, ?string $inheritanceLevel = null) {
        $this->sut = $sut;
        if ($inheritanceLevel !== null && ($sut instanceOf $inheritanceLevel) === false) {
            throw new Exception('Invalid Inheritance Level');
        }
        $this->inheritanceLevel = $inheritanceLevel;
    }

    private function getAgent(): Callable {
        return function ($method, ...$args) {
            if (method_exists($this, $method) === false || is_callable([$this, $method]) === false) {
                throw new Exception(sprintf('Method %s does not exist in class %s', $method, self::class));
            }

            return $this->$method(...$args);
        };
    }

    public function __call(string $method, $args) {
        $agent = $this->getAgent();
        $executor = ($this->inheritanceLevel === null)
            ? $agent->bindTo($this->sut, $this->sut::class)
            : $agent->bindTo($this->sut, $this->inheritanceLevel);

        return $executor($method, ...$args);
    }
}

So now when we want to test the private method show() that exists in the parent Greeting class of our ExtendedGreeting that’s under test, we need to specify the additional argument in our SpyMaster constructor:


$testGreeting = new SpyMaster($sut, Greeting::class);

and the execution of the show() method remains the same:


$result = $testGreeting->show(addition: 'Mark');

Some developers would argue that you shouldn’t unit test private methods anyway, that directly accessing the internal implementation of a class is an anti-pattern.

Many say that you should only test them indirectly, by testing the public methods that call them. But if you’re practising TDD (Test Driven Development) then you might well build those private methods as building blocks before doing an work on the public (API) methods. If you can’t unit test the building block private methods, then you can’t test while you’re doing that development.

Others would argue that if private methods need testing, then they should be refactored out into their own class. While this is always an option (and may be appropriate in many circumstances) it does create a new class with a public API for something that should logically remain private to the original class (and PHP doesn’t support “friend” classes to control that public access).

Whether we should unit test private methods really comes down to the complexity of the method. If the method contains business logic, or implements mathematical formulae, then it should be tested thoroughly; and if we choose not to refactor it into a separate class, then we should be testing those internal methods.

So in those circumstances where we do need to unit test private methods, this SpyMaster approach provides us with a mechanic to do so, and an alternative to using Reflection that (at least to my mind) is cleaner and more intuitive when reading the tests (and always remember that unit tests are a part of the documentation for your code).


My SpyMaster library on Github currently only provides the functionality to read and modify private and protected properties in a class, for testing changes to the object state; but at some point soon, I’ll update it to provide this additional functionality for executing private and protected methods as well.

This entry was posted in PHP and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s