Skip to content

Instantly share code, notes, and snippets.

@Quilted
Last active April 21, 2018 10:52
Show Gist options
  • Save Quilted/1ac94e717b50c486b3ed to your computer and use it in GitHub Desktop.
Save Quilted/1ac94e717b50c486b3ed to your computer and use it in GitHub Desktop.
How to setup Drupal extension for Behat

Set up Behat testing for Drupal

(1) Create a tests folder. Perform the following install tasks within that folder unless otherwise specified.

(2) Follow directions to install the Behat Drupal extension.

(3) Add the following lines to the main repo .gitignore:

# Ignore behat files that can be easily initialized
tests/bin
tests/vendor

# Ignore test results
test_results/
tests/debug/

# Ignore behat config file that needs to be instance-specific
tests/behat.yml

# Don't ignore the parts of the behat DrupalExtension we have patched
!tests/vendor/drupal/drupal-extension/src/Drupal/Driver/Cores/Drupal7.php
!tests/vendor/drupal/drupal-extension/src/Drupal/DrupalExtension/Context/DrupalContext.php
!tests/vendor/drupal/drupal-extension/src/Drupal/DrupalExtension/Extension.php

(4) Create a default.behat.yml:

default:
  paths:
    features: 'features'
  extensions:
    Behat\MinkExtension\Extension:
      goutte: ~
      selenium2: ~
      base_url: [LOCAL DEVELOPMENT URL]
      files_path: 'files/'
    Drupal\DrupalExtension\Extension:
      blackbox: ~
      api_driver: drupal
      drush:
        alias: [LOCAL DEVELOPMENT DRUSH ALIAS]-local
      drupal:
        drupal_root: '../www'
      subcontexts:
        paths:
          - '../www/sites/all/modules'
      selectors:
        error_message_selector: .messages.error
        success_message_selector: .messages.status
      text:
        log_out: "Log Out"
      debug_files_path: 'debug/'

(5) Create a behat.yml based on the default.behat.yml.

(6) Set up a drush alias for the project in ~/.drush called [PROJECT SHORT ID].aliases.drushrc.php:

$aliases['local'] = array(
    'root' => '[PATH TO PROJECT]/www',
    'uri'  =>  '[PROJECT ALIAS]',
    'db-url' => 'mysql://[DB USERNAME]:[DB PASSWORD]@localhost/[DB NAME]'
);

Optionally also set up for staging and live by adding the following lines:

'remote-host' => '[URL or IP]',
'remote-user' => '[SSH USERNAME]',

(7) Add the following functions to FeatureContext.php:

  /**
   * Save the mail system settings.
   *
   * @var
   */
  public $mailSystemSettings;

  /**
   * Initializes context.
   * Every scenario gets its own context object.
   *
   * @param array $parameters context parameters (set them up through behat.yml)
   */
  public function __construct(array $parameters) {}

  /** @BeforeScenario */
  public function saveMailSystemSettings($event) {
    // Save the origial mail system settings.
    $settings = mailsystem_get();
    $this->mailSystemSettings = $settings;

    // Set the test system.
    mailsystem_set(array(mailsystem_default_id() => 'TestingMailSystem'));

    // Flush the email buffer, allowing us to reuse this step definition to clear existing mail.
    variable_set('drupal_test_email_collector', array());
  }

  /** @AfterScenario */
  public function resetMailSystemSettings($event) {
    // Restore the original mail system settings.
    mailsystem_set($this->mailSystemSettings);
  }

  /** @AfterStep */
  public function takeScreenshotOnFail(StepEvent $event) {
    if ($event->hasException()) {
      $this->takeScreenshot(TRUE);
    }
  }

  /**
   * @Given /^I take a screenshot$/
   */
  public function takeScreenshot($fail = FALSE) {
    $driver = $this->getSession()->getDriver();
    $class = get_class($driver);
    $filepath = $this->getDrupalParameter('debug_files_path');
      if ($fail) {
        $filename = sprintf('%s_%s_%s-FAIL', $this->getMinkParameter('browser_name'), date('c'), uniqid('', true));
      }
      else {
        $filename = sprintf('%s_%s_%s-DEBUG', $this->getMinkParameter('browser_name'), date('c'), uniqid('', true));
      }

    if ($class == 'Behat\Mink\Driver\GoutteDriver') {

      // @TODO: Get wkhtmltoimage to work.
      // @see https://groups.google.com/forum/#!topic/behat/dbkzdOLzF6s
      // @see http://extensions.behat.org/symfony2/

      // $html = $this->getSession()->getPage()->getContent();
      // // Generating an Image using KnP's Snappy
      // $image = new \Knp\Snappy\Image();
      // $image->generateFromHtml($html, $filepath . '/' . sprintf('goutte_%s_%s.%s', date('c'), uniqid('', true), 'png'));

      $html = $this->getSession()->getDriver()->getContent();
      $filepath = $filepath ? $filepath : (ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir());
      file_put_contents($filepath . '/' . $filename . '.html', $html);
    }
    elseif ($class == 'Behat\Mink\Driver\Selenium2Driver') {
      $this->saveScreenshot($filename . '.png', $filepath);
    }
  }

  /**
   * @Given /^(?:a|an) "([^"]*)" node with the following fields:$/
   */
  public function anNodeWithTheFollowingFields($type, TableNode $fields) {
    $node = new stdClass();
    $node->type = $type;
    $node->language = 'und';
    foreach ($fields->getRowsHash() as $field => $value) {
      switch ($field) {
        case 'author':
          $user = user_load_by_name($value);
          if (!empty($user)) {
            $node->uid = $user->uid;
          }
          break;

        // case 'field_project_banner_image':
        //   $path = $this->getMinkParameter('files_path') . $value;
        //   $file_contents = file_get_contents($path);
        //   $dirname = "private://$field";
        //   if (file_prepare_directory($dirname, FILE_CREATE_DIRECTORY)) {
        //     $file = file_save_data(
        //       $file_contents,
        //       "private://$field/$value",
        //       FILE_EXISTS_REPLACE
        //     );
        //     $node->{$field} = array(
        //       'filename' => $file->filename,
        //       'uri' => $file->uri,
        //       'fid' => $file->fid,
        //       'display' => 1
        //     );
        //   }
        //   break;

        default:
          $node->{$field} = $value;
      }
    }

    $this->dispatcher->dispatch('beforeNodeCreate', new EntityEvent($this, $node));
    $saved = $this->getDriver()->createNode($node);
    $this->dispatcher->dispatch('afterNodeCreate', new EntityEvent($this, $saved));
    $this->nodes[] = $saved;

    // Set internal browser on the node.
    $this->getSession()->visit($this->locatePath('/node/' . $saved->nid));
  }

  /**
   * @Given /^a user with the following fields:$/
   */
  public function aUserWithTheFollowingFields(TableNode $fields) {
    $row_hash = $fields->getRowsHash();
    if (isset($row_hash['name'])) {
      $user = user_load_by_name($row_hash['name']);
      $new = FALSE;

      if (empty($user)) {
        $user = (object) array();
        $new = TRUE;
      }

      foreach ($row_hash as $field => $value) {
        switch ($field) {
          case 'roles':
            $role_names = explode(',', $value);
            $rids = array();
            foreach ($role_names as $role_name) {
              $role = user_role_load_by_name($role_name);
              if ($role) {
                $rids[] = $role->rid;
              }
            }

            if (!empty($rids)) {
              $user->roles = $rids;
            }
            else {
              throw new \Exception(sprintf('No roles matched %s', $value));
            }

            break;

          // case 'field_user_first_name':
          // case 'field_user_last_name':
          //   $user->{$field}[LANGUAGE_NONE][0]['value'] = $value;
          //   $user->{$field}[LANGUAGE_NONE][0]['safe_value'] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
          //   $user->{$field}[LANGUAGE_NONE][0]['format'] = NULL;
          //   break;

          default:
            $user->{$field} = $value;
        }
      }

      // Set a password.
      if (!isset($user->pass)) {
        $user->pass = Random::name();
      }

      if ($new) {
        $this->dispatcher->dispatch('beforeUserCreate', new EntityEvent($this, $user));
        $this->getDriver()->userCreate($user);
        $this->dispatcher->dispatch('afterUserCreate', new EntityEvent($this, $user));
      }
      else {
        user_save($user);
      }

      $this->addUser($user);
    }
  }

  /**
   * @Then /^I wait for "([^"]*)" ms$/
   */
  public function iWaitForMs($ms) {
    $this->getSession()->wait($ms);
  }

  /**
   * Helper function to find nodes by type and title.
   *
   * @param $invert Set to TRUE to return errors if the node is found
   *
   * @return node
   */
  public function nodeLoadByTitle($type, $title, $invert = FALSE) {
    $query = db_select('node', 'n');
    $query->condition('n.title', $title);
    $query->condition('n.type', $type);
    $query->condition('n.status', 1);
    $query->fields('n', array('nid'));
    $query_result = $query->execute();
    $nid = NULL;

    foreach ($query_result as $record) {
      $nid = $record->nid;
    }

    $node_not_found = TRUE;

    if (!is_null($nid)) {
      $node = node_load($nid);
      if ($node) {
        $node_not_found = FALSE;
      }
    }

    if ($invert) {
      if (!$node_not_found) {
        throw new \Exception(sprintf('A %s node with the title %s exists and it should not.', $type, $title));
      }
    }
    else {
      if ($node_not_found) {
        throw new \Exception(sprintf('There is no %s node with the title %s.', $type, $title));
      }
      return $node;
    }
  }

  /**
   * Helper function to find users by username.
   *
   * @param $invert Set to TRUE to return errors if the user is found
   *
   * @return user
   */
  public function userLoadByUsername($username, $invert = FALSE) {
    $query = db_select('users', 'u');
    $query->condition('u.name', $username);
    $query->condition('u.status', 1);
    $query->fields('u', array('uid'));
    $query_result = $query->execute();
    $uid = NULL;

    foreach ($query_result as $record) {
      $uid = $record->uid;
    }

    $not_found = TRUE;

    if (!is_null($uid)) {
      $user = user_load($uid);
      if ($user) {
        $not_found = FALSE;
      }
    }

    if ($invert) {
      if (!$not_found) {
        throw new \Exception(sprintf('A user with the username %s exists and it should not.', $username));
      }
    }
    else {
      if ($not_found) {
        throw new \Exception(sprintf('There is no user with the title %s.', $title));
      }
      return $user;
    }
  }

  /**
   * @When /^I go to the "([^"]*)" form for the "([^"]*)" node with the title "([^"]*)"$/
   */
  public function iGoToTheFormForTheNodeWithTheTitle($form_type, $type, $title) {
    $node = $this->nodeLoadByTitle($type, $title);
    $path = 'node/' . $node->nid . '/' . strtolower($form_type);
    $this->getSession()->visit($this->locatePath($path));
  }

  /**
   * @When /^I go to the "([^"]*)" node with the title "([^"]*)"$/
   */
  public function iGoToTheNodeWithTheTitle($type, $title) {
    $node = $this->nodeLoadByTitle($type, $title);
    $path = 'node/' . $node->nid;
    $this->getSession()->visit($this->locatePath($path));
  }

  /**
   * @Then /^I should be on the "([^"]*)" node with the title "([^"]*)"$/
   */
  public function iShouldBeOnTheNodeWithTheTitle($type, $title) {
    $node = $this->nodeLoadByTitle($type, $title);
    $path = 'node/' . $node->nid;
    $alias = drupal_get_path_alias($path);
    return new Then("I should be on \"$alias\"");
  }

  /**
   * @Given /^I associate the "([^"]*)" node with the title "([^"]*)" in the entity reference field "([^"]*)"$/
   */
  public function iAssociateTheNodeWithTheTitleInTheEntityReferenceField($type, $title, $field) {
    $node = $this->nodeLoadByTitle($type, $title);
    $formatted_entity_reference = $node->title . ' (' . $node->nid . ')';
    return new When("I fill in \"$formatted_entity_reference\" for \"$field\"");
  }

  /**
   * @Then /^the user "([^"]*)" should have the "([^"]*)" drupal role$/
   */
  public function theUserShouldHaveTheDrupalRole($username, $role) {
    $user = $this->userLoadByUsername($username);
    if (!in_array($role, $user->roles)) {
      throw new \Exception(sprintf('The user with the username %s does not have the role %s.', $username, $role));
    }
  }

  /**
   * @Then /^the user "([^"]*)" should not have the "([^"]*)" drupal role$/
   */
  public function theUserShouldNotHaveTheDrupalRole($username, $role) {
    $user = $this->userLoadByUsername($username);
    if (in_array($role, $user->roles)) {
      throw new \Exception(sprintf('The user with the username %s has the role %s.', $username, $role));
    }
  }

(8) Add the following to the includes at the top of features/bootstrap/FeatureContext.php:

use Behat\Behat\Event\StepEvent;
use Behat\Behat\Context\Step\Then;
use Behat\Behat\Context\Step\When;
use Drupal\DrupalExtension\Event\EntityEvent;

(9) Modify vendor/drupal/drupal-extension/src/Drupal/DrupalExtension/Context/DrupalContext.php:

Change protected $users = array(); to public $users = array(); and add the following:

/**
 * Helper function to add users to the temporary storage.
 */
public function addUser($user) {
  if (!isset($this->users[$user->name])) {
    $this->users[$user->name] = $user;
  }
}

/**
 * Helper function to get users.
 */
public function getUserByName($username) {
  foreach ($this->users as $name => $user) {
    if ($name == $username) {
      return $user;
    }
  }
}

(10) Modify vendor/drupal/drupal-extension/src/Drupal/DrupalExtension/Extension.php:

Add the following after the arrayNode('region_map')-> block:

scalarNode('debug_files_path')->
    defaultValue('debug')->
end()->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment