Testing Protected/Private Methods (PHPUnit)

PROTECTED/PRIVATE METHOD TESTING

If you look at the second part of this series, you will notice that we instantiate our class to be tested via a regular new call. You may also be left wondering how to test protected or private methods if you are unable to access them directly via the instantiated object ( $url->someProtectedMethod() ).

Usually the answer would be, “You do not test protected/private methods directly.”. Since anything non-public is only accessible within the scope of the class, we assume that your class’s public methods (its API) will interact with them, so in the end you are actually indirectly testing these methods anyway.

Of course, there are always exceptions to the rule: What if you are testing an abstract class that defines a protected methods but does not actually interact with it?

What if you want to test different scenarios for a particular method and do not have the opportunity to go through your public methods?

I will explain the process!

Stupid user class

Create a new file at ./phpUnitTutorial/User.php and paste the following:

!! Let me be very clear: the User class is not a good class. Using md5() for !! passwords should be avoided at all costs! In fact, it is a pretty bad class !! overall. That said, it provides a very simple and easy to grasp example of what I !! am teaching.

<?php

namespace phpUnitTutorial;

class User
{
    const MIN_PASS_LENGTH = 4;

    private $user = array();

    public function __construct(array $user)
    {
        $this->user = $user;
    }

    public function getUser()
    {
        return $this->user;
    }

    public function setPassword($password)
    {
        if (strlen($password) < self::MIN_PASS_LENGTH) {
            return false;
        }

        $this->user['password'] = $this->cryptPassword($password);

        return true;
    }

    private function cryptPassword($password)
    {
        return md5($password);
    }
}

Our test would instantiate the User class using $user = new User($details); .

You can access the ::setPassword() method, but are unable to call ::cryptPassword() - but in this case, you do not have to! The fact that your public method interacts with the private method is enough to say, “This method is tested.“, at least with this particular code!

So, how would you create your test for this method? You can see the constructor and ::setPassword() methods both require a parameter. PHPUnit requires no special magic to work with method parameters, as you will soon see.

Creating your test

Create your empty test at ./phpUnitTutorial/Test/UserTest.php and setup the skeleton:

<?php

namespace phpUnitTutorial\Test;

class UserTest extends \PHPUnit_Framework_TestCase
{
    //
}

Running your test suite will result in an error:

1) Warning
No tests found in class "phpUnitTutorial\Test\UserTest".

This is great because it tells us PHPUnit has picked up this test and it is ready for actual code!

We should identify what we would like to test before we go any further. As the phpUnitTutorial\User class is very simple, we can quickly see two scenarios:

  1. ::setPassword() returns true when password is set, and
  2. ::getUser() returns the user array, which would contain the new password which we would then compare against expected result.

We will start with ::setPassword() returning true . Create the empty method:

<?php
// ...

public function testSetPasswordReturnsTrueWhenPasswordSuccessfullySet()
{
    //
}

The phpUnitTutorial\User constructor is expecting a parameter, so define it before instantiating the object:

<?php

namespace phpUnitTutorial\Test;

use phpUnitTutorial\User;

class UserTest extends \PHPUnit_Framework_TestCase
{
    public function testSetPasswordReturnsTrueWhenPasswordSuccessfullySet()
    {
        $details = array();

        $user = new User($details);
    }
}

Note I have added a use statement.

We will now define the parameter required for the ::setPassword() method, and then call it:

<?php
// ...

public function testSetPasswordReturnsTrueWhenPasswordSuccessfullySet()
{
    $details = array();

    $user = new User($details);

    $password = 'fubar';

    $result = $user->setPassword($password);
}

We expect $result to equal true , and I know just the assertion to use, assertTrue() ! Here is our completed test:

<?php
// ...

public function testSetPasswordReturnsTrueWhenPasswordSuccessfullySet()
{
    $details = array();

    $user = new User($details);

    $password = 'fubar';

    $result = $user->setPassword($password);

    $this->assertTrue($result);
}

If you run your test suite, you will get a nice green bar for your troubles.

We can now focus on testing ::getUser() , which is a one-line method so it should be very simple to test, right? Well… not exactly. You see, the whole reason to even test ::getUser() is to give us access to the private, and therefor inaccessible $user property. We want to verify that the $user property has values that we expect - like passwords structured in the correct way.

What this means is that by testing ::getUser() we are also going to end up testing ::__construct() , ::setPassword() and ::cryptPassword() .

Here is our empty test:

<?php
// ...

public function testGetUserReturnsUserWithExpectedValues()
{
    //
}

The only thing we can really test in our particular scenario is that the password that was created by ::cryptPassword() matches our expectations. First, set up the method similar to how we did with ::testSetPasswordReturnsTrueWhenPasswordSuccessfullySet() :

<?php
// ...

public function testGetUserReturnsUserWithExpectedValues()
{
    $details = array();

    $user = new User($details);

    $password = 'fubar';

    $user->setPassword($password);
}

We do not catch the result of ::setPassword() because we are going to assume it passed. If it did not pass, we will know for sure in the next steps.

We know our raw password is fubar . We can see that this password is “hashed” in ::setPassword() using md5() . Therefor we can define what we expect our result to actually be:

$expectedPasswordResult = '5185e8b8fd8a71fc80545e144f91faf2';

We then call ::getUser() to get the user in its current state:

$currentUser = $user->getUser();

What are we expecting our test to actually do?

We are expecting ::getUser() to return an array, and we want to compare the password key to our expected value. The perfect assertion for this would be assertEquals() . Here is our completed test:

<?php
// ...

public function testGetUserReturnsUserWithExpectedValues()
{
    $details = array();

    $user = new User($details);

    $password = 'fubar';

    $user->setPassword($password);

    $expectedPasswordResult = '5185e8b8fd8a71fc80545e144f91faf2';

    $currentUser = $user->getUser();

    $this->assertEquals($expectedPasswordResult, $currentUser['password']);
}

PHPUnit seems to have liked our test:

OK (9 tests, 9 assertions)

Targeting private/protected methods directly

What if we simply want to write more scenarios for a protected method? What if we do not want to go through the public API directly, and instead want to interact solely with the protected method?

There are scenarios where this would be a legitimate need. I will not go through those possible scenarios today (I will in an upcoming part of my series!), but suffice it to say that with a little creative thinking, you can access your instantiated objects’ privates rather easily:

<?php
// ...

/**
 * Call protected/private method of a class.
 *
 * @param object &$object    Instantiated object that we will run method on.
 * @param string $methodName Method name to call
 * @param array  $parameters Array of parameters to pass into method.
 *
 * @return mixed Method return.
 */
public function invokeMethod(&$object, $methodName, array $parameters = array())
{
    $reflection = new \ReflectionClass(get_class($object));
    $method = $reflection->getMethod($methodName);
    $method->setAccessible(true);

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

Using invokeMethod() you can easily call your private or protected methods directly without having to go through public methods.

To use, you simply do

<?php
// ...

$this->invokeMethod($user, 'cryptPassword', array('passwordToCrypt'));

This would be the equivalent of simply typing

<?php
// ...

$user->cryptPassword('passwordToCrypt');

assuming ::cryptPassword() were public.

Refer:
https://jtreminio.com/blog/unit-testing-tutorial-part-iii-testing-protected-private-methods-coverage-reports-and-crap/