Saturday, November 14, 2015

Using PHPUnit with CakePHP

This post will walk you through some of the basics of getting started with PHPUnit and CakePHP.

Adding PHPUnit


After creating a new CakePHP project, open your composer.json file and find the following lines:

    "suggest": {
        "phpunit/phpunit": "Allows automated tests to be run without system-wide install."
    },

Move this line up to the "require-dev" section:

    "require-dev": {
        "psy/psysh": "@stable",
        "cakephp/debug_kit": "~3.0",
        "cakephp/bake": "~1.0",
        "phpunit/phpunit": "*",
        "phpunit/php-invoker": "*"
    },

Next, run "composer update" to update your CakePHP project.

Database Setup


In this example, we have a users table defined in MySQL as follows:

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(255),
    role VARCHAR(20),
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL
);

This table will reside in your "production" database; you should create a "test" database as well (there is no need to create the table in the test database).

Edit config/app.php and put in the references to both databases:

    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            /**
             * CakePHP will use the default DB port based on the driver selected
             * MySQL on MAMP uses port 8889, MAMP users will want to uncomment
             * the following line and set the port accordingly
             */
            //'port' => 'nonstandard_port_number',
            'username' => 'root',
            'password' => '*********',
            'database' => 'mydatabase',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
...
        /**
         * The test connection is used during the test suite.
         */
        'test' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            //'port' => 'nonstandard_port_number',
            'username' => 'root',
            'password' => '*********',
            'database' => 'mydatabase_test',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
...

Now you're ready to bake your users table.

CRUD Controller Tests


The following are examples of the test segments usable in the controller of a CRUD application.  The tests in the controller are considered integration tests because their actions span multiple components.

The bake process will create stubs for you.

Fixtures


Fixtures provide your test database with test data.  After baking your users table, access tests/Fixture/UsersFixture.php and make sure you have at least three rows of test data:

    /**
     * Records
     *
     * @var array
     */
    public $records = [
        [
            'id' => 1,
            'username' => 'john.doe',
            'password' => 'loremipsum',
            'role' => 'admin',
            'created' => '2015-08-08 20:51:50',
            'modified' => '2015-08-08 20:51:50'
        ],
        [
            'id' => 2,
            'username' => 'jane.doe',
            'password' => 'loremipsum',
            'role' => 'editor',
            'created' => '2015-08-08 20:51:50',
            'modified' => '2015-08-08 20:51:50'
        ],
        [
            'id' => 3,
            'username' => 'jack.doe',
            'password' => 'loremipsum',
            'role' => 'viewer',
            'created' => '2015-08-08 20:51:50',
            'modified' => '2015-08-08 20:51:50'
        ],
    ];

Adding a few extra rows is the only thing needed in tests/Fixture/UsersFixture.php for the rest of the items shown in this document.

Users List


The bake process will have also created for you the file  tests/TestCase/Controller/UsersControllerTest.php.  Edit this file and make sure that you have the following use statements:

use App\Controller\UsersController;
use Cake\TestSuite\IntegrationTestCase;
use Cake\ORM\TableRegistry;

Alter testIndex() as follows:

    public function testIndex()
    {
        $this->get('/users?page=1');

        // Check for a 2xx response code

        $this->assertResponseOk();

        // Assert partial response content

        $this->assertResponseContains('john.doe');
    }

In the example above, we've done a get for the first page of the users list.  First we check that the page responded with assertResponseOk(); next, we verify that content from one of the rows that we know should be there is in fact displayed with assertResponseContains('john.doe').

Users View


To test that the "veiw" portion of our CRUD process is working, edit testView() and add the following:

    public function testView()
    {
        $this->get('/users/view/2');

        // Check for a 2xx response code

        $this->assertResponseOk();

        // Assert partial response content

        $this->assertResponseContains('jane.doe');
    }

This is almost identical to what we did for testIndex() except we're getting a specific row.

Users Add


The testAdd() method is a little more detailed.  Edit it and add the following:

    public function testAdd()
    {
        $this->get('/users/add');

        // Check for a 2xx response code

        $this->assertResponseOk();

        $data = [

            'id' => 15,
            'username' => 'ken.kitchen',
            'password' => 'qwerty',
            'created' => time(),
            'modified' => time()
        ];
        $this->post('/users/add', $data);

        // Check for a 2xx response code

        $this->assertResponseSuccess();

        // Assert view variables

        $users = TableRegistry::get('Users');
        $query = $users->find()->where(['username' => $data['username']]);
        $this->assertEquals(1, $query->count());
    }

First, we get the "add" page and verify that it responded.  Next, $data is created with information for the new row.  A post is invoked with $data as a parameter, and we verify that a non-error response was received with assertResponseSuccess().

Finally, we query the Users table for the row that we just created and verify, with count(), that a row exists.

Users Edit


The "edit" portion of this tutorial is still under development.

Users Delete


Finally, edit the testDelete() method and add the following:

    public function testDelete()
    {
        $this->delete('/users/delete/3');

        // Check for a 2xx/3xx response code

        $this->assertResponseSuccess();

        $users = TableRegistry::get('Users');

        $data = $users->find()->where(['id' => 3]);
        $this->assertEquals(0, $data->count());
    }

In this example, we're passing "3" into our get to invoke the page that deletes the row with that id.  Next, we attempt to count() the rows where "id = 3" with the expectation that there will be no rows returned.

Running your tests


With all of the test code added, run PHPUnit from the command line in the root folder of your project:

$ vendor/bin/phpunit

Your results should look something like this:



In this example, some of our tests show as incomplete" because, at this point, we've yet to edit the model tests.  However, all of the new assertions that we created in this tutorial ran successfully.