Ratty
Ratty

Reputation: 446

Properly Testing a simple class with Private Methods

Description: I have a simple class that creates a symlink to a directory of uploaded files that are only available to registered members. It uses the current users session id in order to generate the random directory for the user. Once the user logs out, the symlink is removed. I would like to unit test the functionality of the class.

Question: How do I go about properly unit testing this class since most functions are private, and I don't see any reason to make them public?

Here is the code for the PHP class:

<?php

namespace Test\BackEnd\MemberBundle\Library;


use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\KernelInterface;

class DirectoryProtector
{
/** @var SessionInterface $_session */
private $_session;

/** @var ContainerInterface $_kernel */
private $_kernel;

/**
 * @param SessionInterface $session
 * @param KernelInterface $kernel
 */
public function __construct( SessionInterface $session, KernelInterface $kernel )
{
    $this->_session = $session;
    $this->_kernel = $kernel;
}

/**
 * @param bool|false $protect
 * Public method to symlink directories
 */
public function protectDirectory($protect = FALSE)
{

    if ($protect) {
        if ( ! $this->doesDirectoryExists())
            symlink($this->getAppDir() . '/uploads', $this->getViewableSessionDirectory());
    } else {
        if ($this->doesDirectoryExists())
            unlink($this->getViewableSessionDirectory());
    }

}

/**
 * @return bool
 * Check to see if viewable session directory exists or not
 */
private function doesDirectoryExists()
{
    if (file_exists($this->getViewableSessionDirectory()))
        return TRUE;

    return FALSE;
}

/**
 * @return string
 * Get viewable session full directory path
 */
private function getViewableSessionDirectory()
{
    return $this->getAppDir() . '/../web/files/' . $this->getSessionId();
}

/**
 * @return string
 * Return app root directory
 */
private function getAppDir()
{
    return $this->_kernel->getRootDir();
}

/**
 * @return string
 * Return session id
 */
private function getSessionId()
{
    return $this->_session->getId();
}

}

Here is the code for the current test class:

<?php

namespace Test\BackEnd\MemberBundle\Tests\Library;

use Test\BackEnd\MemberBundle\Library\DirectoryProtector;

class DirectoryProtectorTest extends \PHPUnit_Framework_TestCase
{

public function testProtectDirectory()
{
    //$this->markTestIncomplete("WIP on protect directory.");
    $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')
        ->getMock();

    $container = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')
        ->getMock();

    /** @var DirectoryProtector $dp */
    $dp = $this->getMockBuilder('Test\BackEnd\MemberBundle\Library\DirectoryProtector')
            ->setConstructorArgs(array($request, $container))
            ->setMethods(array(
                'getViewableSessionDirectory',
                'getAppDir',
                'getSessionId'
            ))
            ->getMock();

    $dp->expects($this->once())
        ->method('doesDirectoryExists')
        ->will($this->returnValue(TRUE));

    $dp->protectDirectory(TRUE);

}

}

Upvotes: 3

Views: 4471

Answers (2)

Piotr Olaszewski
Piotr Olaszewski

Reputation: 6204

I think your approach is incorrect. You shouldn't mock method, but objects that are passed by injection in constructor.

For the SessionInterface you can pass MockArraySessionStorage. For ContainerInterface I don't know which class you are using, so I add some demo example. Test case:

public function testProtectDirectory()
{
    $session = new MockArraySessionStorage();
    $session->setId('123123');
    $kernel = \Ouzo\Tests\Mock\Mock::create('ContainerInterface');
    \Ouzo\Tests\Mock\Mock::when($kernel)->getRootDir()->thenReturn('/root_dir');
    $directoryProtector = new DirectoryProtector($session, $kernel);

    $directoryProtector->protectDirectory(true);

    //asserts
}

and your methods will be returning:

getViewableSessionDirectory -> /root_dir/../web/files/123123

getAppDir -> /root_dir

And remember changing methods accessible is always bad idea.

PS. For mocking I use utils form Ouzo framework. Of course you can use other mocking framework.

Upvotes: 1

Absalon Valdes
Absalon Valdes

Reputation: 910

From https://phpunit.de/manual/current/en/test-doubles.html

Limitation: final, private, and static methods

Please note that final, private and static methods cannot be stubbed or mocked. They are ignored by PHPUnit's test double functionality and retain their original behavior.

Is not a good practice to unit test private or protected methods. You should test the public API. Private methods are supposed to be tested indirectly through the API. That said, you can make the method public with reflection:

$instance = new DirectoryProtector(...);

$ref = new \ReflectionClass('DirectoryProtector');

$method = $ref->getMethod('doesDirectoryExists');
$method->setAccessible(true);

$this->assertTrue($method->invoke($instance));

Upvotes: 5

Related Questions