Integration Testing V - Integration Tests for MODX Processors

Creating integration tests for MODX processors


In the previous article in this series, we looked at helper methods in Codeception. In this one we'll see how to write integration test code for processors.


MODX logo

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


The modUserCreateProcessor Class

The processor we're testing is located here:

core/model/modx/processors/security/user/create.class.php

If you look at that class with an eye to testing, you'll see that it's extraordinarily difficult to test. Its various methods are very tightly coupled and there is little or no dependency injection. It clearly wasn't developed using Test-driven Development (TDD).

This test class is a good example of developing tests for legacy code. We're able to test most of the methods, but in doing so, we often have to call other methods of the class that shouldn't need to be called. (We've skipped some methods to keep down the length of this article.) That's a clear sign of a lack of dependency injection. We should be able to test a method in isolation, but in most cases we can't. We can't even use mocks or stubs because we actually need the other methods to execute.

Another issue is that much of the work of this class is performed in the classes it extends. For example, except for the userGroup methods, there's no save() method that actually saves the user object and/or its attached user profile object. That method is hidden in one of the parent::... calls, but it's difficult to guess which one.


Background on MODX Users

In MODX, the modUser object is very light. It stores the username, password, active status, and a few other fields (listed here). That information is stored in the 'modx_users' table. (Actually, the modx_ prefix is the default. It can be changed during setup).

The other user data is stored in the modUserProfile table, which holds the email, fullname, phone, address, gender, and some other fields (listed here).

The modUserCreateProcessor creates and saves both the modUser object and its associated modUserProfile object, which complicates our test somewhat.


The T10_UserProcessorTest.php File

To create the test, give this command from the _build directory:

codecept generate:test integration T10_UserProcessor

Open that file in your editor and replace everything with this code (also available at GitHub here):

<?php
use Codeception\Util\Fixtures;

class T10UserProcessorTest extends \Codeception\Test\Unit
{
    /**
     * @var \IntegrationTester
     */
    protected $tester;
    protected $processor;
    /** @var modX $modx */
    public $modx;
    protected $fields = array(
        'username' => 'Joe99',
        'password' => 'somepassword',
        'passwordnotifymethod' => 'x',
        'email' => 'joe99@hotmail.com',
    );


    protected function _before()
    {
        require_once 'C:/xampp/htdocs/test/core/model/modx/modProcessor.class.php';
        /* Get test file from dev. environment */
        require_once 'C:/xampp/htdocs/test/core/model/modx/processors/security/user/create.class.php';
        $this->modx = Fixtures::get('modx');
        assertInstanceOf('modX', $this->modx);

        $this->processor = modUserCreateProcessor::getInstance($this->modx,
            'modUserCreateProcessor', $this->fields);
        assertInstanceof('modUserCreateProcessor', $this->processor);
        $this->tester->removeUsers($this->modx, array('Joe99'));
    }

    protected function _after()
    {
        $this->tester->removeUsers($this->modx, array('Joe99'));
    }

    public function testClassKey() {
        $p = $this->processor;
        assertEquals('modUser', $p->classKey);
    }

    public function testInitialize()
    {
        /** @var modUserCreateProcessor $p */
        $p = $this->processor;
        $p->initialize();
        assertInstanceOf('modUser', $p->object);
        $props = array(
            'class_key' => 'modUser',
            'blocked' => false,
            'active' => false,
        );

        foreach ($props as $prop => $value) {
            assertEquals($value, $p->getProperty($prop));
        }

        $fields = array(
            'objectType' => 'user',
            'beforeSaveEvent' => 'OnBeforeUserFormSave',
            'afterSaveEvent' => 'OnUserFormSave',
        );
        foreach( $fields as $field => $value) {
            assertTrue($p->$field == $value);
        }
    }

    public function testBeforeSave() {
        /** @var modUserCreateProcessor $p */
        $p = $this->processor;
        $p->initialize();
        $result = $p->beforeSave();
        assertTrue($result, print_r($result, true));
        $object = $p->object;
        assertInstanceOf('modUser', $object);
        $username = $object->get('username');
        assertEquals('Joe99', $username);
        $profile = $object->getOne('Profile');
        assertInstanceOf('modUserProfile', $profile);
        $email = $profile->get('email');
        assertEquals('joe99@hotmail.com', $email);
    }

    public function testAddProfile() {
        /** @var modUserCreateProcessor $p */
        $p = $this->processor;
        $p->initialize();
        $profile = $p->addProfile();
        assertInstanceOf('modUserProfile', $profile);
        $object = $p->object;
        assertInstanceOf('modUser', $object);
        $profile = $object->getOne('Profile');
        assertInstanceOf('modUserProfile', $profile);
        $email = $profile->get('email');
        assertEquals('joe99@hotmail.com', $email);
    }

    public function testAfterSave() {
        $I = $this->tester;
        /** @var modUserCreateProcessor $p */
        $p = $this->processor;
        $p->initialize();
        $p->process();
        $result = $p->afterSave();
        assertTrue($result);
        $userTable = $this->_getTableName('modUser');
        $profileTable = $this->_getTableName('modUserProfile');
        $I->seeInDatabase($userTable, array('username' => 'Joe99'));
        $I->seeInDatabase($profileTable, array('email' => 'joe99@hotmail.com'));
    }

    /** Test full processor action with runProcessor() */
    public function testFull() {
        $I = $this->tester;
        $fields = $this->fields;
        /* Bypass MODX bug */
        $options = array('processors_path' => 'C:/xampp/htdocs/test/core/model/modx/processors/security/');
        $result = $this->modx->runProcessor('user/create', $fields, $options);
        assertInstanceOf('modProcessorResponse', $result);
        $userTable = $this->_getTableName('modUser');
        $profileTable = $this->_getTableName('modUserProfile');
        $I->seeInDatabase($userTable, array('username' => 'Joe99'));
        $I->seeInDatabase($profileTable, array('email' => 'joe99@hotmail.com'));
    }


    /**
     * Utility function to remove back-ticks from table name
     *
     * @param $class
     * @return string
     */
    public function _getTableName($class) {
        $tableName = $this->modx->getTableName($class);
        return str_replace('`', '', $tableName);
    }
}

Notes on the Code

If you've been following along in these articles, the code should be fairly self-explanatory, but a few parts need a little discussion.

The first is the tablename stuff. In order to use our seeInDatabase() tests, we need to give the table name, but the actual name of the table will depend on what the user chose for the table prefix when running setup. The default prefix is modx_, so the typical table name for the MODX User object (modUser is the modx_users table, but if the user has chosen another prefix, it will be named something else.

To help with this, MODX (which knows the prefix) provides the getTableName('classname;) method. There's still a hitch, though getTableName() assumes that you're going to use the table name in a query, so it returns the name surrounded by backticks, making it unusable by seeInDatabase(). So we use this code in our tests:

$userTable = $this->_getTableName('modUser');
$profileTable = $this->_getTableName('modUserProfile');
$I->seeInDatabase($userTable, array('username' => 'Joe99'));
$I->seeInDatabase($profileTable, array('email' => 'joe99@hotmail.com'));

The _getTableName function is our own utility function that calls MODX's getTableName(), removes the back-ticks, and returns the result, which we can then use in seeInDatabase().

We hard-coded the table names in an earlier integration test, which should have worked fine for you because you left the default prefix alone during setup, but for tests in a real project where some users may have used a different prefix for their MODX install, getting the correct table name is essential. Feel free to go back and add the correct process to the T8_UserDbTest.php file.

The second quirk is in this code:

/* Bypass MODX bug */
$options = array('processors_path' =>
    'C:/xampp/htdocs/test/core/model/modx/processors/security/');
$result = $this->modx->runProcessor('user/create', $fields, $options);

The MODX class runProcessor() method calls include_once to load the processor class, which is located under the processors directory at security/user/create.class.php. Normally, we'd call $modx->runProcessor('security/user/create'). The processor class returns its own name, modUserCreateProcessor the first time we include it, so that works fine if we only call it once.

Unfortunately, we've already included that class earlier in our tests. On subsequent calls, include_once returns 1 to indicate that the class is already included rather than returning the class name. MODX is smart enough to detect that return, but it guesses the class name based on the path we send in the first parameter. That means it creates a modSecurityUserCreateProcessor, which is not actually the name of our class and things then fall apart. This only happens because the class file is in a subdirectory rather than directly under the processors directory.

The workaround for this bug is to send the processors_path in the $options array (see the code just above). We've added the security/ directory to the end of the processors path, then called runProcessor() with user/create. MODX finds that class file by tacking on user/create.class.php to the end of the path we sent in the options array. That works. It also guesses the name of the class based on the first parameter, user/create, so we've fooled it into getting the class name right. This bug is fixed in MODX 3, but files are moved around in that version, so some of our tests would have to be modified to have the correct paths to run in MODX 3.

Our test above is incomplete in various ways. If it were a full test, it would be much longer. We'd have to create and remove user groups to test the method that handles adding users to user groups. We'd also have to add quite a few extra lines of code (and possibly a data provider) to make sure the processor handles invalid user data properly. Adding those tests is left as an exercise for the reader.


Wrapping Up

That's it for the unit and integration tests. At this point, if you've created all the tests and classes we've written about, you should be able to run them all in one pass by giving this command in the _build directory.

codecept run unit,integration

You should see something like this at the end of the run:

Time: 3.64 seconds, Memory: 16.00 MB
OK (135 tests, 382 assertions)

If you scroll up and look at the lists, you'll see that Codeception runs the tests in each suite in alphabetical order. That's why the T10_ tests run ahead of the other numbered integration tests (T1 comes ahead of T6 in the alphabet).

If you add this line to the settings: section of codeception.yml in the _build directory, Codeception will run the test files in random order:

shuffle: true

Running several times with the shuffle option on is a good way to make sure none of the tests depend on things that happen in other tests. All software tests should be independent of one another.


Coming Up

In the next article, we'll take a brief look at functional testing.



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)