Listening for calls with an observer

As User::validatePassword() is now using Security::validatePassword() in its implementation in a transparent way, we don't want to expose any of this when setting the password to whoever is going to use the User model.

So, we'd like to think that when setting the password, our implementation will use Security::generatePasswordHash() in some way, so that when calling User::validatePassword(), we close the circle and everything should work without having to worry too much about encryption schemes and what not.

An immediate, somewhat logical, but quite abused way to implement a test that could cover this bit is the following:

public function testSetPasswordEncryptsThePasswordCorrectly()
{
    $clearTextPassword = 'some password';
    $encryptedPassword = 'encrypted password';

    // here, we need to stub our security component

    $this->_user->setPassword($clearTextPassword);

    $this->assertNotEquals(
        $clearTextPassword, $this->_user->password
    );
    $this->assertEquals(
        $encryptedPassword, $this->_user->password
    );
}

Let's stop one second and understand what we're doing here: ideally we want to set a password and read it back encrypted, doing the related and needed assertions. This means that we are both testing the setter and the getter of the password in the same test, which, once again, defies the basic principle of doing unit testing.

As a side note, however we implement the stubbing of the security component, our logic won't look much different from the initial implementation we had at the beginning of this chapter.

Introducing mocking

Mocking refers to the act of replacing an object with a test double that verifies the expectations, for instance ensuring that a method has been called. This seems to meet our needs exactly.

In a proper black box scenario, we won't know what setPassword() does and we would need to rely on the code coverage purely to understand if we've covered any possible branch of the programming flow, as previously said in Chapter 4, Isolated Component Testing with PHPUnit.

Purely as an example for our purposes, we want to make sure that when calling setPassword(), we call Security::generatePasswordHash() at least once and that the argument is passed over to the method without any modification.

Let's try the following approach to test this:

public function testSetPasswordCallsGeneratePasswordHash()
{
    $clearTextPassword = 'some password';

    $security = $this->getMockBuilder(
'yiiaseSecurity')

        ->getMock(
);
    $security->expects($this->once())
        ->method('generatePasswordHash')
        ->with($this->equalTo($clearTextPassword));
    Yii::$app->set('security', $security);

    $this->_user->setPassword($clearTextPassword);
}

As you might have noticed, we don't have any specific assertion in this test. Our mocked class will just mark the test as passed once its method has been called with the specified value.

Getting to know the Yii virtual attributes

In the example we just discussed, it would have been great if we could have hidden the functionality of transforming the cleartext password into an hash from the user.

There are multiple reasons why this isn't happening, but the most important of them is that Yii already provides a quite interesting and well-done system for virtual attributes. This system is in place since Yii 1, and once you get used to it, you can achieve satisfying results.

By implementing a model that inherits from yiiaseComponent, such as ActiveRecord or Model, you will also inherit the already implemented magic functions __get() and __set() that help you deal with virtual attributes. This ends up being particularly useful when you are in need of creating additional attributes without any effort.

Let's see a more practical example.

Let's assume that our User model had a first name and a last name field in the database, but we would need to create the full name attribute without adding a new column in the user table:

namespace appmodels;

class User extends ActiveRecord
{
    /**
     * Getter for fullname
     */
    public function getFullname()
    {
        return $this->firstname . ' ' . $this->lastname;
    }

    // rest of the class
}

So, we can access the field as if it was a normal attribute of the class:

public function testGetFullnameReturnsTheCorrectValue()
{
    $user = new User;
    $user->firstname = 'Rainer';
    $user->lastname = 'Wolfcastle';

    $this->assertEquals(
        $user->firstname . ' ' . $user->lastname,
        $user->fullname
    );
}

Plain and simple public attributes work as you would expect them to. In the following snippets of code, I'm introducing a new class Dog, purely for example purposes, which extends from Model:

namespace appmodels;

use Yii;
use yiiaseModel

class Dog extends Model
{
    public $age;
}

Therefore, our tests would pass without problems:

public function testDogAgeIsRecordedCorrectly()
{
    $expectedAge = 7;
    $dog = new Dog;
    $dog->age = $expectedAge;

    $this->assertEquals($expectedAge, $dog->age);
}

This shouldn't be a surprise to you at all, but let's see what happens if we have both:

namespace appmodels;

class Dog extends ActiveRecord
{
    const AGE_MULTIPLIER = 7;
    public $age;

    public function setAge($age){
        // let's record it in dog years
        $this->age = $age * self::AGE_MULTIPLIER;
    }

    // rest of the class
}

Now, we are expecting setAge() to be triggered on assignment, while reading directly the value of the public attribute:

public function testDogAgeIsRecordedInDogYears()
{

    $dog = new Dog;
    $dog->age = 8;

    $this->assertEquals(
        56, 
        $dog->age
    );
}

However, running this test will only reveal the sad truth:

$ ../vendor/bin/codecept run unit models/DogTest.php

1) testscodeceptionunitmodelsDogTest::testAgeIsRecordedInDogYears
Failed asserting that 8 matches expected 56.

Yes, the test is exactly the same.

Having getters and setters automatically handled by our classes comes at an expense. The sequence of checks that are performed by the magic setter can be summarized with the following:

BaseActiveRecord::__set($name, $value)
  if (BaseActiveRecord::hasAttribute($name))
      $this->_attributes[$name] = $value;
  else
      Component::__set($name, $value)
        if (method_exists($this, 'set'.$name))
            $this->'set'.$name($value);
        if (method_exist($this, 'get'.$name))
            throw new InvalidCallException(...);
        else
            throw new UnknownPropertyException(...);

If you have implemented a model extending from yiiaseActiveRecord, its base class will first check if the attribute is already available as a table column; otherwise, it will pass the call over to Component::__set(). This method is available not only for models extending from yiiaseModel, but also for any other that implicitly inherits from yiiaseComponent, such as behaviors and events.

Following this, we can see that the setter will ensure that the 'set'.$name method is available in our class, and if there's only a getter, then it will raise an exception.

In our initial definition of the firstname getter, we could have had the following additional test:

/**
 * @expectedException yiiaseInvalidCallException
 */
public function testSetFullnameThrowsException()
{
    $user = new User;
    $user->firstname = 'Fido';
    $user->lastname = 'Smith';

    // setter not available
    $user->fullname = 'Something Else';
}

There are a couple or more things regarding events and behaviors done in the setter, but we won't touch them as of now.

So, going back to our setPassword() conundrum, we need to be aware that if we were to trigger the magic method by using $user->password for the left assignment, this won't trigger our method.

So, the best solution would ideally have been to call the stored password in a more declarative way, such as password_hash.

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

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