Isolating components with stubs

The problem we are facing right now is that we don't really want to use the actual security component, as it's not part of the test itself. Keep in mind that we're working in a black box environment, and we don't know what other dependencies the security component might have in the future. We just need to ensure that our implemented method will behave correctly, given the interface of the (fake) object works as expected. We can later add an integration method to ensure that the security component actually works, but that's a completely different matter.

In order to do that, PHPUnit provides an interesting system for stubbing and mocking classes and injecting them into your application to provide a more controlled environment. Generically, these are normally called test doubles and the method used to create them is through the Mock Builder.

The latest versions of PHPUnit (4.x or above) suggest the use of the Mock Builder in order to configure the stub and behavior. Previously, this was done through a lengthy series of arguments to be passed to it. I won't indulge in saying that if you're working with PHPUnit 3.x or earlier versions, it might be time to upgrade!

Note

Please note that the final, private, and static methods cannot be stubbed or mocked as a PHPUnit test double functionality does not support this.

Stubbing in particular refers to the practice of replacing an object with a test double that might return configured values.

So, how are we doing this? I've decided to take the approach of using a separate private function to delegate the stubbing logic to a reusable piece of code:

/**
 * Mocks the Yii Security module,
 * so we can make it return what we need.
 *
 * @param string $expectedPassword the password used for encoding
 *                                 also used for validating if the
 *                                 second parameter is not set
 */
private function _mockYiiSecurity($expectedPassword)
{
    $security = $this->getMockBuilder(
'yiiaseSecurity')
        ->getMock();

We start by creating the stub of our security class by using getMockBuilder(). By default, the Mock Builder will replace all the class methods with test doubles that return null.

We can also decide selectively which ones are to be replaced by specifying them in an array and then passing it to setMethods(); for example: setMethods(['validatePassword', 'generatePasswordHash']).

We can also pass null to it; we can avoid any method from having a test double, but without it, we won't be able to set any expectation.

Tip

If the class you're mocking performs unneeded initializations in the __constructor() method, you can disable it by using disableOriginalConstructor() or passing custom arguments with setConstructorArguments(). There are more methods that can be applied to modify the behavior of the Mock Builder; refer to the following documentation: https://phpunit.de/manual/current/en/test-doubles.html#test-doubles.stubs.

Any method that is a test double can be configured and be monitored with the use of expects():

    $security->expects($this->any())
        ->method('validatePassword')
        ->with($expectedPassword)
        ->willReturn(true);

    $security->expects($this->any())
        ->method('generatePasswordHash')
        ->with($expectedPassword)
        ->willReturn($expectedPassword);

    Yii::$app->set('security', $security);
}

This seems to be pretty much straightforward to read: any (any()) time the method (method()) 'validatePassword' is invoked with (with()) the $expectedPassword, it will return (willReturn()) true.

There are a number of ways you can configure your replaced functions: having them return only once a certain value, or different values in consecutive calls, or throw exceptions, when invoked.

Note

Much more is available and documented in the official PHPUnit documentation available at https://phpunit.de/manual/current/en/test-doubles.html.

We can then implement the negative test to cover a wrong password passed to validatePassword() with the logic we wanted:

/**
 * @expectedException yiiaseInvalidParamException
 */
public function testValidatePasswordThrowsInvalidParamExceptionIfPasswordIsIncorrect() {
    $password = 'some password';
    $wrongPassword = 'some other password';
    $this->_mockYiiSecurity($password, $wrongPassword);

    $this->_user->password = $password;
    $this->_user->validatePassword($wrongPassword);
}

For this to happen, we will need to slightly refactor the private method we just implemented:

/**
 * Mocks the Yii Security module,
 * so we can make it returns what we need.
 *
 * @param string $expectedPassword the password used for encoding
 *                                 also used for validating if the
 *                                 second parameter is not set
 * @param mixed $wrongPassword  if passed, validatePassword will
 *                              throw an InvalidParamException
 *                              when presenting this string.
 */
private function _mockYiiSecurity($expectedPassword, $wrongPassword = false)
{
    $security = $this->getMockBuilder(
'yiiaseSecurity')
        ->getMock()
    );
    if ($wrongPassword) {
        $security->expects($this->any())
            ->method('validatePassword')
            ->with($wrongPassword)
            ->willThrowException(new InvalidParamException());
    } else {
        $security->expects($this->any())
            ->method('validatePassword')
            ->with($expectedPassword)
            ->willReturn(true);
    }
    $security->expects($this->any())
        ->method('generatePasswordHash')
        ->with($expectedPassword)
        ->willReturn($expectedPassword);

    Yii::$app->set('security', $security);
}

Here, we can finally see how to configure our replaced method to throw the exception using willThrowException(). With it, we can ensure an exception is being thrown by a method; for this reason, tests that check for exceptions are to be separated from the others.

Note

The official documentation provides a more detailed explanation of the use of the Mock Builder API and it is available at https://phpunit.de/manual/current/en/test-doubles.html.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset