Integration Testing III - Integration Testing in MODX using the Codeception Db Module

Using MODX as a fixture to do integration testing with the Codeception Db module


In the previous article in this series, we installed the MODX CMS in the test directory and created a Codeception fixture to make the $modx object available in our tests. In this one, we'll create some tests that use MODX and test the MODX modUser object. We'll also see Codeception's Db module in action, and we'll look at utility functions in Codeception.


MODX logo

This article assumes that you've installed and configured Codeception, PhpUnit, and MODX as described in the previous articles. The test files and classes are available at GitHub here.


The modUser Class

In previous articles, we've been testing various versions of our own User class. Since we now have the MODX CMS installed, we're going to switch over to testing the MODX modUser class. The modUser class does not store profile information like email, phone, etc., those are stored in another table. The fields of the modUser class here. Many of the fields are not required.

We've skipped testing our own User class save() method, but now that we're doing integration testing, we'll test that method of the modUser object. To do that, we're going to need some way to access the database that's independent of the system we're testing.


The Codeception Db Module

The Codeception Db module provides a simple way to communicate with several common database platforms. Before we go much further, let's enable that Db module. The first step is to install it.

Enter this command in your terminal:

composer require codeception/module-db 1.* --dev

Once the Db module has been installed, we need to enable it in one of the Codeception configuration (.yml files. Add this code to the integration.suite.yml file:

modules:
    enabled:
        - \Helper\Integration
        - Db:
            dsn: 'mysql:host=localhost;dbname=test'
            user: 'JoeTester'
            password: 'TesterPassword'

            # dump: 'tests/_data/test.sql'
            # populate: true
            # cleanup: true
            # reconnect: true
            # waitlock: 30

The configuration code above makes the methods of the Codeception Db class available to the actor used in the tests. We'll come back to that when we look at the test code.

Remember that in the previous article, we set up a MODX database called test with the credentials listed above. Follow the steps in that article if you haven't done so already.

Create the new test file and do a build by issuing these commands in the _build directory:

codecept generate:test integration T8_UserDb
codecept build

The build step is primarily to make sure Codeception knows to attach the methods of the Db module to the actor used in our test (IntegrationTester). Running build isn't always necessary, but it never hurts, and it will also clean up some temporary files created by the testing platform.


T8_UserDbTest.php File

Replace the content T8_UserDbTest.php file in the _build/tests/integration directory with this code (also available at GitHub - here):

<?php
use Codeception\Util\Fixtures;

class T8_UserDbTest extends \Codeception\Test\Unit
{
    /**  @var $tester IntegrationTester */
    protected $tester;

    /** @var modX $modx */
    public $modx;

    protected $usernames = array(
        'User1','User2',
    );

    protected function _before()
    {
        $this->modx = Fixtures::get('modx');
        assertTrue($this->modx instanceof modX);
        $this->_removeUsers($this->modx, $this->usernames);
    }

    protected function _after()
    {
        $this->_removeUsers($this->modx, $this->usernames);
    }


    /**
     * @param modX $modx
     * @param array $usernames
     */
    protected function _removeUsers($modx, $usernames) {

        foreach($usernames as $username) {
            $user = $modx->getObject('modUser', array('username' => $username));
            if ($user) {
                $user->remove();
            }
        }
    }
    // tests
    public function testWorking()
    {
        $I = $this->tester;
        assertTrue(true);
        $I->dontSeeInDatabase('modx_users', array('username' => 'User1'));
        $I->dontSeeInDatabase('modx_users', array('username' => 'User2'));
    }

    public function testSaveUser() {
        /** @var @var modUser $user */
        $I = $this->tester;
        $user = $this->modx->newObject('modUser');
        $user->set('username', 'User1');
        assertTrue($user->save());
        $I->seeInDatabase('modx_users', array('username' => 'User1'));
    }

    public function testUpdateUser() {
        $I = $this->tester;
        $I->dontSeeInDatabase('modx_users', array('username' => 'User1'));
        $I->dontSeeInDatabase('modx_users', array('username' => 'User2'));
        $user = $this->modx->newObject('modUser');
        $user->set('username', 'User1');
        assertTrue($user->save());
        $I->seeInDatabase('modx_users', array('username' => 'User1'));
        $user = $this->modx->getObject('modUser', array('username' => 'User1'));
        assertNotEmpty($user);
        assertInstanceOf('modUser', $user);

        $user->set('username', 'User2');
        assertTrue($user->save());
        $I->dontSeeInDatabase('modx_users', array('username' => 'User1'));
        $I->seeInDatabase('modx_users', array('username' => 'User2'));
    }

    public function testDeleteUser() {
        $I = $this->tester;
        $I->dontSeeInDatabase('modx_users', array('username' => 'User1'));
        $user = $this->modx->newObject('modUser');
        $user->set('username', 'User1');
        assertTrue($user->save());
        $I->seeInDatabase('modx_users', array('username' => 'User1'));
        $user->remove();
        $I->dontSeeInDatabase('modx_users', array('username' => 'User1'));
    }
}

At the top of the file, we see the necessary use Codeception\Util\Fixtures; line, which makes the call to Fixtures::get() in the _before method work to get an instance of the $modx object.

Also in the _before() and after() methods, is a call to the utility function _removeUsers(). The underscore in the function name tells Codeception and PhpUnit not to run the function as a test. We can call the function directly, but the testing platform will ignore it as it works its way through the test methods. Utility functions can be helpful if you need a short function to perform tasks that are not really tests. Anything you can do in PHP, you can do in a test file.

The _removeUsers() method simply checks the database for our two test users by username and removes them if they are there. This will mean the users will be removed before any test in the file because the _before() method runs before each test method in the file. It's a best practice in software testing to make sure the state of the system being tested is the same before every method in a test file, so it's best to undo anything that your tests do.

You might think this is unnecessary since you can undo the changes in the test method itself. This doesn't always work, though. If a crash occurs because of a PHP error, Codeception and PhpStorm do their best to complete the code in any _after() or tearDown() methods, but sometimes that's impossible. That means that the next time you make a test run, the system may not be in the state you thought it was.

For example, suppose you try to create a user when that same user is already in the database from an earlier failed run. The creation will fail, and it may take some time to figure out why. Even if you know why, it's still a pain to go into the DB and delete the user manually to fix things. It's better to just remove any leftover users both before and after any test run.


SQL Dump

Another way to handle the problem of making sure the database is in a particular state before and after testing is to create a database dump by "Exporting" the DB in PhpMyAdmin to a .sql file or the equivalent. If you look back at the lines in the integration.suite.yml file, you'll see that we commented out these lines:

            # dump: 'tests/_data/test.sql'
            # populate: true
            # cleanup: true
            # reconnect: true
            # waitlock: 30

If you uncomment the lines by removing the # ahead of them, Codeception will use the specified dump file to populate the database and let cleanup return it to its initial state after each test. This is great in theory. Unfortunately, it's painfully slow. On my computer, the T8_UserDbTest file runs in less than a second. Using a database dump to populate and clean up after the test takes about 60 times as long. Worse yet, it will populate and roll back the database before *every* test, even those that don't need it. If you use a data provider, it will populate and then restore the entire DB after each run through the method that uses the data provider — in other words, the database will be populated and restored once for each case in the array returned from the data provider.

If you have a complex set of tests, you may want to populate and restore the database before and after all your tests, but even then it would be way faster to do so at the command line, like this:

// dump DB to sql file
sudo mysqldump -u JoeTester -p TesterPassword test > backup.sql

// restore DB from sql file
mysql -u JoeTester -p TesterPassword test < backup.sql

The Actor

We've set the actor in the the integration.suite.yml file to IntegrationTester. That matches up with our annotation in the test file:

/**  @var $tester IntegrationTester */
protected $tester;

The annotation is for code editors like PhpStorm, which will use that information to autocomplete the actor's methods and warn you about any methods that are not available in the actor's class.

In many of the methods we've set a local variable, $I, like this:

$I = $this->tester;

This isn't strictly necessary, since we could use $this->tester everywhere. It's done for two reasons. One is that $I is shorter to type then $this->tester. Another is that using $this->tester everywhere would create some grammatically awkward code:

$this->tester->seeInDatabase();
$this->tester->dontSeeInDatabase()

The $I variable is used very often in functional and acceptance tests, so we'll be seeing a lot more of it.

Methods like seeInDatabase() and dontSeeInDataBase are attached to the actor (IntegrationTester) used in the test. Because we enabled the Db module, we can call any of its methods with $this->tester->methodName(), or (Once we've set $I = $this->tester) we can use $I->methodName(). The methods of the Db module are documented here.


Why Two Database Connections

We have two separate DB connections in our test. One from the Codeception Db module, and the other from the $modx object. This seems redundant because everything we do with one, we could do with the other. The reason we use two separate connections is that we don't want our assertions to use the same system we're in the process of testing.


Back to the Code

The code in our test file above should be fairly obvious. We call the save() and remove() methods, then check using $seeInDatabase() and dontSeeInDatabase() to make sure the objects are there or missing as required by the test.


A Final Note

Notice that our _removeUsers() method provides a nice example of dependency injection (DI). The method needs to use two external dependencies, the $modx object, and an array of usernames. We've used dependency injection by sending both of the external dependencies as parameters in the method call. One advantage of this use of DI is that we can change the array of usernames at the beginning, and it will be used throughout the test. We can also send different arrays of user names to the method as needed.

Another reason DI is useful here is that it makes the _removeUsers() method self-contained. We can move it somewhere else (even to another class) without modifying it at all. This will come in handy in the next article when we move that method out of our test class and make it a Codeception helper method.


Coming Up

In the next article, we'll take a look at Helper methods in Codeception.



For more information on how to use MODX to create a web site, see my web site Bob's Guides, or better yet, buy my book: MODX: The Official Guide.

Looking for high-quality, MODX-friendly hosting? As of May 2016, Bob's Guides is hosted at A2 hosting. (More information in the box below.)



Comments (0)


Please login to comment.

  (Login)