Testing private/protected PHP methods using the Reflection API

In object oriented PHP, class properties and methods have visibility levels which determine if the property or method can be accessed outside its class, or in classes that extend its class. There are 3 visibility levels in OOP PHP namely:

As seen from the definitions above, protected/private methods cannot be tested as easily as public methods. This is because test classes cannot directly access protected/private methods. Let’s setup a simple code example to visualize this problem.

More often than not, the logic in your private methods will also have been tested when testing the public methods as they utilise the private methods, but on the off chance that you still need to test private methods, we will use the Reflection API to achieve this.



Run the following commands to setup a quick php composer project with phpunit for testing,

You can use the default composer options and enter “no” when asked if you want to setup your dependencies interactively, as this is just a demo project.


mkdir testProject && \
cd testProject && \
composer init && \
composer require --dev  phpunit/phpunit



The above command simply creates a directory called “testProject” in your present working directory, it then sets up a composer project using the “composer init” command in the testProject, and lastly it installs phpunit as a dev dependency.

Next run the following command so we can set up our directory structure

mkdir src && \
touch src/Book.php && \
mkdir tests && \
touch tests/BookTest.php



We should also add autoloading to our project, because who doesn’t autoload? 😅. Add the following snippet to your composer.json file.

"autoload": {
    "psr-4": {
        "App\\": "src/"
    }
},
"autoload-dev": {
    "psr-4": {
        "Tests\\": "tests/"
    }
}

Copy the following (obviously simplified) code snippet into “src/Book.php”.

<?php

namespace App;

class Book {
    /**
     * @var
     */
    public $name;

    /**
     * @var
     */
    protected $pages;

    /**
     * @var
     */
    private $isbn;

    /**
     * Book constructor.
     * @param string $name
     * @param int $pages
     * @param string $isbn
     */
    public function __construct(string $name, int $pages, string $isbn)
    {
        $this->name = $name;
        $this->pages = $pages;
        $this->isbn = $isbn;
    }

    /**
     * @param $name
     */
    public function setName($name) {
        $this->name = $name;
    }

    /**
     * @param $pages
     */
    public function setPages($pages) {
        $this->pages = $pages;
    }

    /**
     * @param $isbn
     */
    public function setIsbn($isbn) {
        $this->isbn = $isbn;
    }

    /**
     * @return mixed
     */
    public function getName() {
        return $this->name;
    }


    /**
     * @return mixed
     */
    protected function getPages() {
        return $this->pages;
    }

    /**
     * @return mixed
     */
    private function getIsbn() {
        return $this->isbn;
    }
}

We just added a few properties and methods that can help us test and see the challenge when trying to test private methods.



Next let’s create a add the following snippet to “tests/BookTest.php”

<?php

namespace Tests;

use App\Book;
use PHPUnit\Framework\TestCase;

class BookTest extends TestCase
{
    public function testItCanGetABooksIsbn()
    {
        $book = new Book("Test Book", 20, "Test Isbn");
        $this->assertEquals("Test Isbn", $book->getIsbn());
    }
}

Run the BookTest class with phpunit tests\, and you should see get an error like this:

image of failed test



The Solution - Reflection API

You know how you can not see your face except it is reflected to you, maybe in a picture, or a “mirror mirror on the wall” ⛄️ 😂, well the same thing goes for PHP objects, a PHP object, class or function cannot know the parameters, methods or properties that make it up except we use the “PHP mirror” which technically is called “REFLECTION”. The simple idea behind reflection is that there is a way for us to reverse engineer classes, interfaces, functions, methods and extensions.

Having explained the issue with testing private/protected methods, let’s now look at a way we can test such methods with the help of PHP’s reflection API.

Add the following method into your BookTest.php

In real world applications, this method should probably be extracted to a trait or your base test case so other tests can use it if needed, but for demo purposes putting it directly in the test file works.


 /**
     * @param $object
     * @param string $method
     * @param array $parameters
     * @return mixed
     * @throws \Exception
     */
    private function callMethod($object, string $method , array $parameters = [])
    {
        try {
            $className = get_class($object);
            $reflection = new \ReflectionClass($className);
        } catch (\ReflectionException $e) {
           throw new \Exception($e->getMessage());
        }

        $method = $reflection->getMethod($method);
        $method->setAccessible(true);

        return $method->invokeArgs($object, $parameters);
    }

The idea behind the callMethod function is simple,

  1. Access the private/protected method on the object to be tested
  2. Set its accessibility too true so it can be invoked.
  3. Invoke the private/protected method with its parameters if any
  4. Return the result.

Now you can update your test method to use callMethod:

public function testItCanGetABooksIsbn()
    {
        $book = new Book("Test Book", 20, "Test Isbn");
        $this->assertEquals("Test Isbn", $this->callMethod($book, 'getIsbn'));
    }

Running the test again, we now see that it passes, and there you have it, you can now test private/protected methods. 🎉

You can find the official documentation for the PHP’s Reflection API here. It’s a lot though, so don’t feel pressured to learn it all, use it as a reference source when you need to work with reflection in PHP.