Component testing of the model

Testing the validation of a model and any further data manipulation, until it reaches the database and comes back, is the basic step in Yii to ensure that the model has clear and well-defined validation rules implemented. This is effectively useful when it comes down to preventing clients from being able to pass additional or wrong data, when interacting with the system.

If you care about security, this is something you might need to investigate a bit further, if you haven't done it already.

Tip

I'd like to stress the position we've taken in the previous statement: We're taking the consumer/client perspective. At this particular moment, we don't know how things are going to be implemented, so it's better to focus on the usage of the model.

So, let's get back to /tests/codeception/unit/ models/UserTest.php: The file should already be there, and it's more or less what you would get by running the following command:

$ ../vendor/bin/codecept generate:phpunit unit models/UserTest
Test was created in /var/www/vhosts//htdocs/tests/unit/UserTest.php

Right now, if you were to run this command, you would end up with a test that you would need to slightly change, so that we could use the Yii infrastructure. In particular, you would need to change the class your test was extending from with the following:

// tests/codeception/unit/UserTest.php
namespace testscodeceptionunitmodels;

use yiicodeceptionTestCase;

class UserTest extends TestCase
{
}

In other words, we need to use the provided yiicodeceptionTestCase class, instead of the PHPUnit default PHPUnit_Framework_TestCase class.

So, let's first sketch down a few tests within our testscodeceptionunitmodelsUserTest class:

// tests/codeception/unit/models/UserTest.php

public function testValidateReturnsFalseIfParametersAreNotSet() {
    $user = new User;
    $this->assertFalse($user->validate(), "New User should not validate");
}

public function testValidateReturnsTrueIfParametersAreSet() {
    $configurationParams = [
        'username' => 'a valid username',
        'password' => 'a valid password',
        'authkey' => 'a valid authkey'
    ];
    $user = new User($configurationParams);
    $this->assertTrue($user->validate(), "User with set parameters should validate");
}

As you can see, knowing what to test requires insight on how Yii works. So, it might be completely fine to actually get the first test sketches completely wrong, if you don't know how things are intended to work.

In the preceding code snippet, we've defined two tests; the first is what we've called the negative and the second is the positive one.

Tip

Please note that passing the second parameter to the various assert commands will help you debug the tests in case they're failing. Writing a descriptive and meaningful message could save time.

Note

In the code snippets of this book, the second parameter of the various assert methods will not be passed, in order to keep the code snippets more compact.

What's testing for PHPUnit

Before we continue with the rest of our tests, let's review what we've got up until here: The test file is a class with a name in the <Component>Test format, which collects all tests relating to the component we want to test; each method of the class is a test of a specific feature, either positive or negative.

Each method/test in the class should have at least one assertion, and PHPUnit provides a long list of assertion statements that you can trigger to assert that an actual value matches an expected value, together with methods to expect for a specific exception.

These methods are provided by the parent class TestCase. You can get the full list at https://phpunit.de/manual/current/en/appendixes.assertions.html.

Some basic assertions are as follows:

  • assertTrue(actualValue) and its opposite assertFalse(...)
  • assertEquals(expectedValue, actualValue) and its opposite assertNotEquals(...)
  • assertNull(actualValue)

The result of your test is based on the output of these methods. You should also try to avoid wrapping some assertions within one or more conditions. Think carefully about what you're trying to achieve and what you're actually testing.

As for exceptions, you need to use some documentation annotation:

Note

PHPUnit uses documentation annotations extensively to cover what's not normally doable with in-test assertions.

On top of what we will see, there's plenty of other functionalities, such as testing dependencies with @depends, @before, and @after or grouping with @group.

For a full list of annotations you can use, head over to https://phpunit.de/manual/current/en/appendixes.annotations.html.

Consider the following example:

/**
 * @expectedException yiiaseInvalidParamException
 */
public function 
testValidatePasswordThrowsInvalidParamExceptionIfPasswordIsIncorrect() {
    $user = new User;
    $user->password = 'some password';

    $user->validatePassword('some other password'),
}

On top of @expectedException, you can also use @expectedExceptionCode and @expectedExceptionMessage, in case you need to ensure that the content of the exception is what you are expecting it to be.

Another way to do this is to use the setExpectedException() method, which might provide a higher level of flexibility when you have more complex exception cases to deal with.

Tip

Although very generic, we can also expect language-specific errors when passing a different type to a method with a typed formal parameter, or when trying to include a non-existing file by using @expectedException PHPUnit_Framework_Error.

Assertion testing in PHPUnit is quite straightforward once you've got a grip on how your class, model, and method are going to be used.

On top of this, PHPUnit provides some clever functionality to help us speed up testing and solve some intricacies. Data providers, fixtures, stubs, and mocks will be covered later on and in Chapter 5, Summoning the Test Doubles.

Testing the methods inherited by IdentityInterface

Now that we know everything we need to in order to start, we would normally decide to implement the rules to make the testValidateReturnsTrueIfParametersAreSet() and testValidateReturnsTrueIfParametersAreNotSet()tests pass, although at this occasion, it seems much easier to just continue sketching the remaining methods that we would need to implement later on, such as getId(), getAuthKey(), validateAuthKey(), findIdentity(), and findIdentityByAccessToken(), plus two more methods that have been implemented and used already, namely validatePassword() and findByUsername(), both used by the LoginForm model.

We can immediately decide to get rid of the simplest methods to cover. We're not going to make any use of the access token, and normally, if we weren't forced to implement the method by the interface, we could have just avoided this bit. In this case, instead, we need to get it sorted and the best way to document this missing functionality is to raise NotSupportedException from the method and expect such an exception:

/**
 * @expectedException yiiaseNotSupportedException
 */
public function testFindIdentityByAccessTokenReturnsTheExpectedObject()
{
    User::findIdentityByAccessToken('anyAccessToken'),
}

Following this method, we test getId():

public function testGetIdReturnsTheExpectedId() {
    $user = new User();
    $user->id = 2;

    $this->assertEquals($expectedId, $user->getId());
}

We can use the exact same logic to test $user->getAuthkey().

While for findIdentity(), we can do the following:

public function testFindIdentityReturnsTheExpectedObject() {
    $expectedAttrs = [
        'username' => 'someone',
        'password' => 'else',
        'authkey' => 'random string'
    ];
    $user = new User($expectedAttrs);

    $this->assertTrue($user->save());

    $expectedAttrs['id'] = $user->id;
    $user = User::findIdentity($expectedAttrs['id']);

    $this->assertNotNull($user);
    $this->assertInstanceOf('yiiwebIdentityInterface', $user);
    $this->assertEquals($expectedAttrs['username'], $user->username);
    $this->assertEquals($expectedAttrs['password'], $user->password);
    $this->assertEquals($expectedAttrs['authkey'], $user->authkey);
}

With findIdentity(), we want to make sure the object returned is the one we were expecting, so our assertions ensure that:

  1. there's a record retrieved
  2. it's of the right class (IdentityInterface is what most of the methods interacting with the user at authentication time will expect it to be)
  3. it contains what we've passed when creating it

Using data providers for more flexibility

The negative test for findIdentity() is quite straightforward:

public function testFindIdentityReturnsNullIfUserIsNotFound() {
    $this->assertNull(User::findIdentity(-1));
}

Implementing a test like this might raise some eyebrows, as we've hardcoded a value, -1, which might not be representative of any actual real-world case.

The best way would be to use a data provider, which can feed our test with a list of values that should make the test pass. This is quite convenient as we can tailor edge cases when it comes down to doing some regression testing on the existing features:

/**
 * @dataProvider nonExistingIdsDataProvider
 */
public function testFindIdentityReturnsNullIfUserIsNotFound(
    $invalidId
) {
    $this->assertNull(User::findIdentity($invalidId));
}

public function nonExistingIdsDataProvider() {
    return [[-1], [null], [30]];
}

In a data provider, each second-level array is a call to the function requesting it and the content of these arrays is the ordered list of the actual parameters of the method.

So, in our preceding case, the test will receive -1, null, and 30 in consecutive invocations.

If we were to use a data provider for our initial test testFindIdentityReturnsTheExpectedObject(), we could test whether the username contains UTF-8 or invalid characters, for instance.

So, using data providers is a good thing! It gives us the ability to use a single test to check more complex situations that require a certain level of flexibility.

But here comes the problem: The database that is used during all tests (with $user->save()) will continue to grow, as there is no instruction to tell it to do otherwise.

As a result, we can add the following to the setUp() function:

// tests/codeception/unit/models/UserTest.php

protected function setUp()
{
    parent::setUp();
    // cleanup the User db
    User::deleteAll();
}

Remember to clean up after yourself: You might be impacting someone else's test. For now, with this call to deleteAll() in place, we are fine.

The setUp() function is called at the beginning before every single test contained in the class. PHPUnit provides several layers of methods for setting things up before one or more tests, and unsetting them after. The sequence of calls can be summed up with the following:

testscodeceptionunitmodelsUserTest::setUpBeforeClass();
   testscodeceptionunitmodelsUserTest::_before();
      testscodeceptionunitmodelsUserTest::setUp();
         testscodeceptionunitmodelsUserTest::testSomething();
      testscodeceptionunitmodelsUserTest::tearDown();
   testscodeceptionunitmodelsUserTest::_after();
testscodeceptionunitmodelsUserTest::tearDownAfterClass();

Here, setUpBeforeClass() is the most external call possible that is run before the class is instantiated. Please also note that _before and _after are Codeception TestCase methods, while the rest are standard PHPUnit methods.

Since we are here, we could also add a test-wide User class that will be instantiated before each test; it can be used by any of our tests. For this to happen, we need to add a private variable and add the related statement, where needed:

// tests/codeception/unit/models/UserTest
/** @var User */
private $_user = null;

protected function setUp()
{
    parent::setUp();
    // setup global User
    $this->_user = new User;
    // cleanup the User db
    User::deleteAll();
}

Now, we just need to amend the relevant tests to use $this->_user when needed.

Tip

Try to keep private variables and methods clearly visible; this could also help you avoid naming conflicts, as we will see when introducing fixtures.

Using fixtures to prepare the database

As we've seen, the data provider solution helps you run the same test with a different dataset each time, which ends up being extremely useful. Another and possibly complimentary solution is to use fixtures that let you preload some well-defined data and keep tests even more simple. This would mean being able to test methods such as User::findIdentity() without having to rely on $user->save(), which is not a part of the test itself.

Fixtures are used to set the database at a fixed/known state so that your tests can run in a controlled environment. In doing this, we will also eliminate the need to delete all users in the setUp function or rely on static values that might be influenced by other previously run tests.

The fixture is just a class that is dynamically loaded in the setUp() method, and you're left with only the task of creating the fixture class and the actual content for the database.

Let's start by creating the fixture class:

// tests/codeception/unit/fixtures/UserFixture.php
namespace appcodeception	estsunitfixtures;

use yii	estActiveFixture;
class UserFixture extends ActiveFixture
{
    public $modelClass = 'appmodelsUser';
}

In this case, we're extending from ActiveFixture as it will provide some additional functionality that might be useful, so the only thing we need to do is to define the model it will mimic. The alternative, as for login forms or other custom-made models, is to extend from yii estFixture, where you have to define the table name by using the public property $tableName. With ActiveFixture, by just defining $className, the fixture will figure out the table name by itself.

The next step is to define the actual fixture that will define the content we want to fill into our database. By default, Yii will try to look for a file named <table_name>.php within the fixtures/data/ folder. The fixture is just a return statement of an array, such as the following:

// tests/codeception/unit/fixtures/data/user.php

return [
    'admin' => [
        'id' => 1,
        'username' => 'admin',
        'password' => Yii::$app->getSecurity()->generatePasswordHash('admin'),
        'authkey' => 'valid authkey'
    ]
];

Each entry of the fixture can be key-indexed to quickly reference it in our tests. You don't also normally need to specify the primary key as they will be automatically created, as in the case of ActiveRecord.

As a last step, we need to implement the fixtures() method to define which fixtures we want to use in our tests. To do so, we can use the following code:

// tests/codeception/unit/models/UserTest.php

public function fixtures() {
    return [
        'user' => UserFixture::className(),
    ];
}

By doing this, our setUp() method will initialize the database with the content of the fixture we've just defined. If we were in need to use more than one fixture for the same fixture class, then we could have specified which fixture to load in the current test by also returning a dataFile key that specified the path of the fixture, as in the following example:

public function fixtures()
{
    return [
        'user' => [
            'class' => UserFixture::className(),
            'dataFile' => '@app/tests/codeception/unit/fixtures/data/userModels.php'
        ]
    ];
}

Now that we have the fixture defined and ready to be used, we can access its content via the $this->user variable (and now you can see why it's better to keep private and public variables well defined and separate). You can normally use it as an array and access the index or key you need, or let it return an ActiveRecord object as with $this->user('admin').

Now, we can see it in action by refactoring our previously implemented test:

public function testFindIdentityReturnsTheExpectedObject() {
    $expectedAttrs = $this->user['admin'];

    /** @var User $user */
    $user = User::findIdentity($expectedAttrs['id']);

    $this->assertNotNull($user);
    $this->assertInstanceOf('yiiwebIdentityInterface', $user);
    $this->assertEquals($expectedAttrs['id'], $user->id);
    $this->assertEquals($expectedAttrs['username'], $user->username);
    $this->assertEquals($expectedAttrs['password'], $user->password);
    $this->assertEquals($expectedAttrs['authkey'], $user->authkey);
}

This way, we can carry on with our tests without worrying about calling save() every time we need to ensure that a record is in the database.

This also means that we won't need to clean up the database, as the fixture will do so for us:

protected function setUp()
{
    parent::setUp();
    $this->_user = new User;
}

Following what we just said, it should be quite straightforward to implement tests for findByUsername() in the same way as we did for findIdentity(). So, I'll leave this for you as an exercise.

Adding the remaining tests

By now, we should have almost all the tests created, apart from the ones covering validateAuthKey(), which you should be able to implement without any particular problem, and validatePassword(), which we will take a closer look at in Chapter 5, Summoning the Test Doubles.

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

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