Acceptance Testing XI - Step Object

Creating the step object for our resource protection acceptance test


In the previous article, we saw an overview of our test and discussed the StepObject class. In this one, we'll create the step object for our test, and look at its structure.


MODX logo

This article assumes that you've installed and configured Composer, Codeception, PhpUnit, MODX, Java, WebDriver, and ChromeDriver as described in earlier articles, and that you've created the acceptance test support files from those articles.


Codeception StepObject

Step objects in Codeception are very much like helpers and page objects. Helpers are intended to help with a particular test or suite of tests. Page objects are to hold information about specific pages, either in the front end, or the back end. Step objects are for use when you have an action that might be used by a number of different tests, such as the creating users, logging in, clearing the cache, and so on.

In previous articles, we created a fairly complete acceptance test for the process of creating users. You can imagine that you might also have separate acceptance tests for creating resources, resource groups, user groups, and roles. This test won't be testing those processes. We'll create all those objects using the methods of a StepObject class we'll call Objects.

Once the objects all exist, we'll test the ACL process by logging in the admin user to create the ACL entry that protects the hidden resource, then log out the admin, and log in each of the non-admin users to see if the ACL entry works as it should.

The first thing we'll do is create the step object that will be used to create the objects used in our test. As we learned in the previous article, Codeception has an interactive command-line process for creating step objects. When you give the command to create the step object, Codeception creates it, and then prompts you for the names of the methods you want in the step object class. Enter just the method names followed by Enter. To end the process, just press the Enter key without naming a method. Here are the steps:

In the _build directory, enter the first line below, then the following method names as prompted (don't type "[Enter]," just press the Enter key). When you're finished, quit by pressing the Enter key without adding a method name.

codecept generate:stepobject acceptance Objects [Enter]
    createRoles [Enter]
    removeRoles [Enter]
    createUserGroups [Enter]
    removeUserGroups [Enter]
    createUsers [Enter]
    removeUsers [Enter]
    createResourceGroups [Enter]
    removeResourceGroups [Enter]
    createResources [Enter]
    removeResources [Enter]
    createCategories [Enter]
    removeCategories [Enter]
    createElements [Enter]
    removeElements[Enter]
    [Enter]

We won't be using the createElements() and removeElements() methods above for the current test, but we'll use them in a future article.

We'll be calling each method in the step object individually, so other tests can create just the objects needed.

Once you've completed the steps above, you should see a new file in the _build/tests/_support/Step/Acceptance directory called Objects.php containing the methods you listed. At the top, you should see this:

namespace Step\Acceptance;

class Objects extends \AcceptanceTester {

We'll call the "create..." methods in our _before() method and the "remove..." methods in our _after() method. That should work fine, since we're not modifying any of these objects in our tests.

Notice that the class is a subclass of the AcceptanceTester class (not the AcceptanceTest class). That means that we're extending the actor used in acceptance tests, not the class of the test itself. It lets us call any of the methods in our Objects.php file with $I->methodName() in our acceptance test, as long as we pass the proper actor in as a function argument (\Step\Acceptance\Objects $I) to our _before() and _after() methods.

We already have a Login PageObject that can log the users in and out. It would probably be more appropriate for that to be a StepObject, but we have enough to do. Feel free to create a login step object later.


The Objects.php File

Because we added all the methods we need when we created the step object, all we need to do now is fill in the ones we want.

Here's the finished code of the _build/tests/_support/Step/Acceptance/Objects.php file (also available at GitHub here).

(It also includes the code to create elements and categories, event though we won't be using those methods for a while.)


<?php
namespace Step\Acceptance;

class Objects extends \AcceptanceTester
{
    protected $categoriesArray = array();

    public function createRoles($modx, $roles)
    {
        $I = $this;

        foreach ($roles as $name) {

            $role = $modx->getObject('modUserGroupRole',
                array('name' => $name));
            if ($role) {
                $role->remove();
            }
            $role = $modx->newObject('modUserGroupRole');
            $role->set('name', $name);
            $role->set('authority', 15);
            $success = $role->save();
            assertTrue($success);
        }
    }

    public function removeRoles($modx, $roles)
    {
        $I = $this;
        foreach($roles as $name) {
            $role = $modx->getObject('modUserGroupRole',
                array('name'=> $name));
            if ($role) {
                $role->remove();
            }
        }
    }

    public function createUserGroups($modx, $groups)
    {
        $I = $this;
        foreach($groups as $name) {
            $group = $modx->getObject('modUserGroup',
                array('name' => $name));
            if ($group) {
                $group->remove();
            }
            $group = $modx->newObject('modUserGroup');
            $group->set('name', $name);
            $success = $group->save();
            assertTrue($success);
        }
    }

    public function removeUserGroups($modx, $groups)
    {
        $I = $this;
        foreach($groups as $name) {
            $group = $modx->getObject('modUserGroup',
                array('name'=> $name));
            if ($group) {
                $group->remove();
            }
        }
    }

    public function _createUser($modx, $fields) {
        /** @var $modx modX */
        // user->joinGroup() crashes with no $_SESSION set
        $_SESSION['dummy'] = 'x';
        $pw = $fields['password'];
        unset($fields['password']);
        $fields['specifiedpassword'] = $pw;
        $fields['confirmpassword'] = $pw;
        $fields['passwordnotifymethod'] = 'x';
        $fields['passwordgenmethod'] = 'x';

        $modx->error->reset();
        $modx->runProcessor('security/user/create', $fields);
        $user = $modx->getObject('modUser',
            array('username' => $fields['username']));
        assertInstanceOf('modUser', $user);
        if ($user) {
            $success = $user->joinGroup(
                $fields['usergroup'], $fields['role']);
            assertTrue($success);
            $user->save();
            /* Give everyone access to the Manager */
            $success = $user->joinGroup('Administrator',
                'Super User');
            assertTrue($success);
        }
    }
    public function createUsers($modx, $users)
    {
        /** @var $modx modX */
        $I = $this;
        foreach ($users as $user) {
            $userObj = $modx->getObject('modUser',
                array(
                    'username' => $user['username']
                ));
            if ($userObj) {
                $userObj->remove();
            }
            $this->_createUser($modx, $user);
        }
    }

    public function removeUsers($modx, $users)
    {
        $I = $this;
        foreach($users as $user) {
            $userObject = $modx->getObject('modUser',
                array('username' => $user['username']));
            if ($userObject) {
                $userObject->remove();
            }
        }
    }

    public function createResourceGroups($modx, $groups)
    {
        $I = $this;
        foreach($groups as $name) {
            $group = $modx->getObject('modResourceGroup', array ('name' => $name));
            if ($group) {
                $group->remove();
            }
            $group = $modx->newObject('modResourceGroup');
            $group->set('name', $name);
            $success = $group->save();
            assertTrue($success);
        }
    }

    public function removeResourceGroups($modx, $groups)
    {
        $I = $this;
        foreach ($groups as $name) {
            $group = $modx->getObject('modResourceGroup',
                array('name' => $name));
            if ($group) {
                $group->remove();
            }
        }
    }

    public function createResources($modx, $resources)
    {
        $I = $this;

        $template = (int) $modx->getOption('default_template',
            null, 0, true);

        foreach($resources as $resource) {
            $r = $modx->getObject('modResource',
                array('alias' => $resource['alias']));
            if ($r) {
                $r->remove();
            }
            $modx->runProcessor('resource/create',
                $resource);

            $r = $modx->getObject('modResource',
                array('alias' => $resource['alias']), false);
            assertInstanceOf('modResource', $r);
            $success = $r->joinGroup($resource['group']);
            assertTrue($success);
        }
    }

    public function removeResources($modx, $resources)
    {
        $I = $this;
        foreach ($resources as $resource) {
            $r = $modx->getObject('modResource',
                array('alias' => $resource['alias']));
            if ($r) {
                $r->remove();
            }
        }
    }

    public function createCategories($modx, $categories)
    {
        $I = $this;
        foreach ($categories as $category) {
            $o = $modx->getObject('modCategory',
                array('category' => $category));
            if ($o) {
                $o->remove();
            }
            $c = $modx->newObject('modCategory');
            $c->set('category', $category);
            $success = $c->save();
            assertTrue($success, 'Failed to save Category');
            /* Set ID of category in $this->categoriesArray */
            $cat = $modx->getObject('modCategory',
                array('category' => $category));
            if ($cat) {
                $this->categoriesArray[$category] = $cat->get('id');
            }
        }
    }

    public function removeCategories($modx, $categories)
    {
        $I = $this;
        foreach( $categories as $category) {
            $c = $modx->getObject('modCategory', array('category' => $category));
            if ($c) {
                $c->remove();
            }
        }
    }

    public function createElements($modx, $elements) {
        $I = $this;
        foreach($elements as $element) {
            $nameField = 'name';
            if ($element['class_key'] === 'modTemplate') {
                $nameField = 'templatename';
                $element[$nameField] = $element['name'];
                unset($element['name']);
            }
            $o = $modx->getObject($element['class_key'],
                array($nameField => $element[$nameField]));
            if ($o) {
                $o->remove();
            }
            $element['category'] =
                $this->categoriesArray[$element['category']];
            $object = $modx->newObject($element['class_key']);
            $object->fromArray($element);
            $object->set('category',$element['category']);
            assertInstanceOf($element['class_key'], $object);
            $success = $object->save();
            assertTrue($success);
        }
    }

    public function removeElements ($modx, $elements) {
        $I = $this;
        foreach($elements as $element) {
            $nameField = $element['class_key'] == 'modTemplate'
                ? 'templatename'
                : 'name';
            $e = $modx->getObject($element['class_key'],
                array($nameField => $element['name']));
            if ($e) {
                $e->remove();
            }
        }
    }
}

Notice that each method contains the line $I = $this;. This line is unnecessary since we're not calling any methods of the $I object. It could be removed, but there's a chance we might want to use it sometime in the future.

Looking at the Code

Notice that the createUser() method doesn't contain data specific to the user being created, such as username and password. That data is actually passed from the main test file, which gets it from the tests/data directory and passes it to the createUsers()method in the second argument. The createUsers() method loops through the user array and passes the data for each user to the createUser() method as a second argument.

The rest of the Objects.php code is fairly straightforward, but I wanted to mention this section of the createUser() method code:

$_SESSION['dummy'] = 'x';
$pw = $fields['password'];
unset($fields['password']);
$fields['specifiedpassword'] = $pw;
$fields['confirmpassword'] = $pw;
$fields['passwordnotifymethod'] = 'x';
$fields['passwordgenmethod'] = 'x';

$modx->error->reset();

This code is necessary because of some unfortunate design choices (made long ago) in the MODX processor that creates users.

The dummy $_SESSION variable is set because even though the processor doesn't need any $_SESSION variables, it checks for them without first making sure the $_SESSION array exists and crashes if the array doesn't exist. This is no problem inside MODX, where a $_SESSION array is always set, but we need to make sure it's set before running our code. (I've reported this bug and it may well be fixed by the time you read this.)

The processor also assumes that the user is being created in the Manager, so it insists on knowing the password generation method (automatic or manual) and the password notification method (email or screen).

It crashes if either of these fields are missing even though we're generating the password ourselves and definitely don't want to send an email or show a popup message on the screen every time we create a user. We set those values to 'x', which basically fools MODX into ignoring them.

The $modx->error->reset(); line is necessary because of another questionable design choice. The processor uses the MODX error object to store the message that's displayed on the screen. It tells the admin that the user has been created and shows the created user's password if it's not sending an email.

This works fine as long as you're only creating one user, but when a second user is created, the processor first checks the error object to see if anything went wrong earlier. If the message is still there, it assumes there's an error and stops the process. So we need to clear the error object with $modx->error->reset(); after creating each user.


Coming Up

We have code to create our objects, but we need to create the data files with the raw data for users and resources. We'll do that in the next article.



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)