Giant druplicon. The pupils in it's eyes are made of Codeception's icons
How we made friends with Codeception and Drupal

Taking into consideration the fact that most of our products are based on Drupal, the tests should also naturally work best with such projects. This is why we decided to complement the standard functionality of Codeception with some new modules dedicated for Drupal.
As in our previous article, all examples listed below will be based on a project based on docker-console, which is why we encourage everybody to read the previous articles first if you didn’t do so yet. If you already have your Codeception project and just want to slightly modify it so that it works better with Drupal, this article is also for you.
When we first took a shot at expanding the functionality of our tests, we had some requirements that did not fully line up with existing modules, which is why the majority of solutions listed below was created by us. They are all based on other, previously available solutions, but we hope you will like the way Droptica expanded them and made them a cohesive whole, available in a blink of an eye in your Codeception project config.

codeception-drupal-bootstrap

The first module presented in this article will be codeception-drupal-bootstrap, which can be found at: https://github.com/droptica/codeception-drupal-bootstrap
This is a fork of a module: https://github.com/Chapabu/codeception-module-drupal (It was modified by us, because in its original form, it did not allow for use in multiple suites at the same time, and additionally, it was not updated for Drupal 8). It allows for using Drupal functions in tests and other Codeception modules. 

Configuration

In order to be able to use this module, you have to configure it in a yaml file for a given suite, e.g. unit.suite.yml.
Unlocking suite for Drupal 7 in configuration.
 

- \Codeception\Module\Drupal7\Drupal7:
            root: '/app/app'
            relative: no

Unlocking configuration for Drupal 8:
   

- \Codeception\Module\Drupal8\Drupal8:
            root: '/app/app/'
            env: 'prod'
            relative: no
            site_dir: 'default'

 
Example

Its use is obvious for anybody working with Drupal. In a recent article, we showed you an example of checking whether a given module is operational using Drupal commands. In this note, we are going to show you another helpful test to check whether features of your website is not overwritten. If you want this example to work with our project, you have to add the features module to your project and export some features You can also fetch files from codeception-drupal branch, where we did that for you already.

<?php

class ExampleUnitTest extends \Codeception\Test\Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;

    protected function _before()
    {
    }

    protected function _after()
    {
    }

    /**   TESTS     */
     
    public function testFeaturesDefault() {

      module_load_include('inc', 'features', 'features.export');

      // Load information about feature.
      $features[] = 'test';
      $features_states = features_get_component_states($features, FALSE, TRUE);

      foreach ($features_states as $feature_name => $feature_state) {
        $overridden_features = array_filter($feature_state, function($item) {
          return $item != FEATURES_DEFAULT;
        });
        $this->assertEquals(TRUE, empty($overridden_features), 'Feature ' . $feature_name . ' is overridden.');
      }
    }

}

codeception-drupal-users

Another module presented in this article is codeception-drupal-users, which can be found at: https://github.com/droptica/codeception-drupal-users . This is a modified drupal-user-registry module – https://github.com/ixis/codeception-module-drupal-user-registry , used for automatic creation of test users while launching a test suite. Using the module, you can quickly create and use test users with specified roles during your tests. By default, all test users will be removed after completing all tests from a given suite (unless specified otherwise). 
The original module uses drush for creating users; however, this method did not allow for filling custom profile fields, which is why our module uses Drupal functions instead of drush for creating new users. Because of that, it is possible for us not only to fill in standard user details but also custom profile fields.

Configuration

The configuration is simple and is all done in a configuration file of a given suite. However, it is necessary to remember about several things. First of all, in order for this module to work, you have to enable codeception-drupal-bootstrap module in a given suite. Additionally, the Entity API (https://www.drupal.org/project/entity) module should be also enabled in the project.

Examples

It is always best to discuss configuration using an example, which is why we added two additional fields and three new roles for users. (If you don’t know how to do this in your project, you can fetch the changes from docker-drupal branch). After enabling the module, we have to specify what kinds of users we want to create. In order to do so, we have to first declare the roles of our users (you can do this the way it is in the example presented below or declare, e.g. roles custom, where the user will have several roles defined in the system). After describing the roles in the project, I also specify the data that will be used to fill our new fields (I could specify them for each user separately). After doing that, we can move on to specifying the exact users we want codeception to create before executing our tests. At the end, we should have a “true” value in “create” if we want our users to be created before executing a given suite. The same applies to “delete” – this deletes our users after executing a test suite. In my case, the users are not deleted so that we can check in the system whether they were created in the first place. If we execute the test twice, the new users will not be duplicated, we will get an error message saying that the user already exists in the system.

- ManageUsers:
            defaultPass: "123"

#Available roles:
#1 anonymous user
#2 authenticated user
#3 administrator
#4 editor
#5 user
#6 nodoby

            roles_admin: &roles_admin
              2: "authenticated user"
              3: administrator

            roles_editor: &roles_editor
              2: "authenticated user"
              4: editor

            roles_user: &roles_user
              2: "authenticated user"
              5: user

            roles_nodoby: &roles_nodoby
              2: "authenticated user"
              6: nodoby

            custom_fields: &custom_fields
              field_inne_pole: 'Fname'
              field_custom_field: 'key1'

            users:
              admin_user:
                name: admin_user
                email: [email protected]
                roles: *roles_admin
                custom_fields:
                  <<: *custom_fields

              editor_user:
                name: editor_user
                email: [email protected]
                roles: *roles_editor
                custom_fields:
                  field_inne_pole: 'Fname2'
                  field_custom_field: 'key2'

              user_user:
                name: user_user
                email: [email protected]
                roles: *roles_user
                custom_fields:
                  <<: *custom_fields

              nodoby_user:
                name: nodoby_user
                email: [email protected]
                roles: *roles_nodoby
                custom_fields:
                  <<: *custom_fields

            create: true                 # Whether to create all defined test users at the start of the suite.
            delete: false                 # Whether to delete all defined test users at the end of the suite.

For Drupal 8, the configuration of the module looks almost identical. The only difference is enabling the module using:

- \Codeception\Module\Drupal8\ManageUsers:

instead of ManageUsers.
Of course, you can use the functions from this module while writing the test case. Among the most useful ones are those that allow us to refer to a test user or a group of users:

  • getAllTestUsers()
  • getTestUsersByRoles($roles = array(), $return_one_user = FALSE)
  • getTestUserByName($username)

codeception-drupal-content-types 

Another one of our Drupal modules is codeception-drupal-content-types, which can be found at: https://github.com/droptica/codeception-drupal-content-types . It was based on: https://github.com/ixis/codeception-drupal-content-types and is used for creating and removing test nodes. 

Configuration

In order to enable: 

 - DrupalContentTypeRegistry:
            contentTypesAutoDump: true
            contentTypesAutoDumpFile: "contentTypes.yml"
            contentTypesFile: "contentTypes.yml"
            customFieldsFile: "contentTypesOverrides.yml"

After doing this, the execution of tests should generate contentTypes.yml file, listing the structure of nodes available in the project. An example of such a file can be seen in the image below.

Structure of nodes

If one of the content types has defined fields such as field collection or others, which comprise nested fields (e.g. location), the contentTypes.yml file will include just the main fields, without nested ones. In order to fill in the nested fields, such as in field collection, you have to include them in contentTypesOverrides.yml file. All people who initiated the project using docker-console unit-tests or merged the code from our example repository should already have this file in their projects. It should contain tips on how to overwrite the given configuration and look like the one below.

Suggestion of tests in representative repository

Example

In our example, we will show you how to create an article type node using this module.

class NewNodeTestCest
{

 /**
   * @var string
   * article_title
   */
  private $article_title;

  /**
   * @var string
   * article_body
   */
  private $article_body;

  /**
   * @var string
   * article_tags
   */
  private $article_tags;

  function __construct() {
    $this->article_title = 'Test article';
    $this->article_body = 'Test article body';
    $this->article_tags = 'Test article tag'; 
  }

  public function _before(AcceptanceTester $I) {
  }

  public function _after(AcceptanceTester $I) {
  }

  /**   TESTS     */

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function newArticle(AcceptanceTester $I, UserSteps $U) {
    $I->wantTo('Test - I can log in as admin and add and delete article');
    $I->amOnPage('/');
    $user = $I->getTestUserByName('admin_user');
    $U->login($user->name, $user->pass);
     $fields_values = array(
      'title' => $this->article_title,
      'body' => $this->article_body,
      'field_tags' => $this->article_tags
    );
    $I->createNode($I, 'article', $fields_values, NULL, FALSE);
    $nid = $I->grabLastCreatedNid($I);
    $I->deleteNodeFromStorage($nid);
    $I->amOnPage('/user/logout');
  }

 }

codeception-drupal-content-types + NodeSteps

In order to make our work with the module even easier, we added some additional supporting functions, which allow us to specify which user we want to use in order to add or remove a given node. The NodeSteps file contains functions that log us in as a given user before adding/removing a node. An example of using these functions can be found below, here we are adding a “page” node as admin_user.

<?php

use Step\Acceptance\UserSteps;
use Step\Acceptance\NodeSteps;

class NewNodeAsUserTestCest
{

 /**
   * @var string
   * page_title
   */
  private $page_title;

  /**
   * @var string
   * page_body
   */
  private $page_body;

  function __construct() {
    $this->page_title = 'Test page';
    $this->page_body = 'Test page body';
  }

  public function _before(AcceptanceTester $I) {
  }

  public function _after(AcceptanceTester $I) {
  }

  /**   TESTS     */

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function newPage(AcceptanceTester $I, UserSteps $U, NodeSteps $N) {
    $I->wantTo('Test - I can add and delete page as admin');
    $I->amOnPage('/');
     $fields_values = array(
      'title' => $this->page_title,
      'body' => $this->page_body
    );
    $nid = $N->createNewNodeAsUser('admin_user', 'article', $fields_values);
    $N->deleteNodeAsUser('admin_user', $nid);
  }

 }

codeception-drupal-storage

The final module presented in today’s article is codeception-drupal-storage, which can be found at: https://github.com/droptica/codeception-drupal-storage
It is used for storing variables and the test nodes we create globally in the entire test suite until we finish executing tests from a given suite.
In this module, you can take advantage of the following methods:

  • setVariableToStorage($name, $value)
  • getVariablesFromStorage($names = array())
  • getVariableFromStorage($name)
  • deleteVariableFromStorage($name)
  • appendNodeToStorage($nid)
  • getAllNodesFromStorage()
  • getNodeFromStorage($nid)
  • deleteNodeFromStorage($nid)

Configuration

The module does not require any complicated configuration, simply enable it by adding the following line to the config file of a given suite.

- SuiteVariablesStorage

Example

As in every case, it is always best to see what the module can do with an example. In our example, presented below, we split creating a node, checking whether it has proper content and removing it altogether into three separate tests, which allowed us to show how we are able to move a given value from one test to another. Of course, the tests don’t have to be in a single file; however, it is vital that they all belong to a single suite because after the suite is executed, all values created during the tests are removed. (Additionally, you can remove a given value at any time using the appropriate method.)

<?php

use Step\Acceptance\UserSteps;
use Step\Acceptance\NodeSteps;

class VariablesStorageTestCest
{

 /**
   * @var string
   * page_title
   */
  private $page_title;

  /**
   * @var string
   * page_body
   */
  private $page_body;

  function __construct() {
    $this->page_title = 'Test page';
    $this->page_body = 'Test page body';
  }

  public function _before(AcceptanceTester $I) {
  }

  public function _after(AcceptanceTester $I) {
  }

  /**   TESTS     */

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function newPage(AcceptanceTester $I, UserSteps $U, NodeSteps $N) {
    $I->wantTo('Test - I can log in as admin and add article');
    $I->amOnPage('/');
     $fields_values = array(
      'title' => $this->page_title,
      'body' => $this->page_body
    );
    $nid = $N->createNewNodeAsUser('admin_user', 'article', $fields_values);
    $I->setVariableToStorage('page_nid', $nid);
  }

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function seeTextOnPage(AcceptanceTester $I) {
    $I->wantTo('Test - I can see page title and body text on test page');
    $nid = $I-> getVariableFromStorage('page_nid');
    $I->amOnPage('/node/'.$nid);
    $I->canSee($this->page_title);
    $I->canSee($this->page_body);
  }

  /**
   * @param \AcceptanceTester $I
   * 
   */
  public function deletePage(AcceptanceTester $I, UserSteps $U, NodeSteps $N) {
    $I->wantTo('Test - I can delete test page');
    $nid = $I-> getVariableFromStorage('page_nid');
    $N->deleteNodeAsUser('admin_user', $nid);
  }

 }

Project files

You can run the examples described in this article by downloading them from the project repository and changing the branch to codeception-drupal.
Project repository:

https://github.com/DropticaExamples/docker-console-project-example

Database dump:

https://www.dropbox.com/s/r0o3u9gjp3dccd4/database.sql.tar.gz?dl=0

Project files:

https://www.dropbox.com/s/hl506wciwj60fds/files.tar.gz?dl=0

 

Helper functions:

Apart from adding modules, we will add special functions to every project, allowing us to better test the website. We already demonstrated these functions when we presented NodeSteps with codeception-drupal-content-types module. (These are some special classes, you can read more about them here: http://codeception.com/docs/06-ReusingTestCode#StepObjects

We will also discuss them in detail in one of our upcoming articles.) You can also write helper functions in files added to every test suite, for example, the “Acceptance” file in _support/Helper or create a new file and write helper functions there, just like in our example with the DrupalHelper file found in the same folder. Regardless of which variant you choose, you should remember to unlock the access to these functions in a given suite in a yaml file, like in the example listed below.

Suite file. Arrow shows line "\Helper\Acceptance" listed under Enabled modules

If you don’t do it, you will have to import it in every file where you will want to use a given function by adding e.g.

use Step\Acceptance\UserSteps;
use Step\Acceptance\NodeSteps;

Conclusion

We hope that you like our small improvements for Drupal. If you are using a solution that is dedicated for Drupal and we haven’t written anything about it yet, let us know – we will gladly expand our configuration with new and cool modules. If you're just starting your adventure with Codeception, we encourage you to experiment and write as many tests as you can on your own, because as you probably already know, “practice makes perfect”. Additionally, we encourage everybody to keep visiting our blog – you can be sure that we will write more about this tool in the future.
 

Need a team of Drupal and PHP web development experts?

Contact us now!