Testing

Vijaya Chandran Mani

@vijaycs85

Overview

  • Introduction

  • Types

  • Sample tests

  • Lessons learned

1. Introduction

What?

  • Code to test code by coder for coder
  • Lives and runs as part of the project codebase
  • Quality assurance at developer level

Why?

  • SDLC - Requirements are not explicit
  • Edge cases are painful
  • Express understanding of the system
  • Developer Documentation
  • Easy to write
  • Cost effective (DEV -> QA -> PROD -> Bug -> Repeat)
  • Impossible to do CI / CD
  • Security updates
  • Software version upgrade

2. Types of testing 

  1. Unit
  2. Kernel
  3. Functional
  4. Functional Javascript
  5. Behat

Structure

PHPUnit

Drupal core

Structure (expanded)

Functional Test Base 

Full list of base class is available here

Folder Structure 

Use case

Contact module

Contact

  • CRUD contact form
  • Compose message
  • Send mail

Contact form view == message add

2.1 Unit tests

Drupal\Tests\UnitTestCase

Mail handler

namespace Drupal\contact;

/**
 * Provides an interface for assembly and dispatch of contact mail messages.
 */
interface MailHandlerInterface {
  /**
   * Sends mail messages as appropriate for a given Message form submission.
   *
   * Can potentially send up to three messages as follows:
   * - To the configured recipient;
   * - Auto-reply to the sender; and
   * - Carbon copy to the sender.
   *
   * @param \Drupal\contact\MessageInterface $message
   *   Submitted message entity.
   * @param \Drupal\Core\Session\AccountInterface $sender
   *   User that submitted the message entity form.
   *
   * @throws \Drupal\contact\MailHandlerException
   *   When unable to determine message recipient.
   */
  public function sendMailMessages(MessageInterface $message, AccountInterface $sender);
}
// contact.services.yml

  contact.mail_handler:
    class: Drupal\contact\MailHandler
    arguments: ['@plugin.manager.mail', '@language_manager',
             '@logger.channel.contact', '@string_translation', '@entity.manager']

Setup

use  Drupal\Core\Routing\RouteProvider;

class MailHandlerTest extends UnitTestCase {
  protected function setUp() {
    parent::setUp();
    $this->mailManager = $this->getMock(MailManagerInterface::class);
    $this->languageManager = $this->getMock(LanguageManagerInterface::class);
    $this->logger = $this->getMock(LoggerInterface::class);
    $this->entityManager = $this->getMock(EntityManagerInterface::class);
    $this->userStorage = $this->getMock(EntityStorageInterface::class);
    $this->entityManager->expects($this->any())
      ->method('getStorage')
      ->with('user')
      ->willReturn($this->userStorage);

    $string_translation = $this->getStringTranslationStub();
    $this->contactMailHandler = new MailHandler($this->mailManager,
      $this->languageManager, $this->logger, $string_translation, $this->entityManager);
    $language = new Language(['id' => 'en']);

    $this->languageManager->expects($this->any())
      ->method('getDefaultLanguage')
      ->will($this->returnValue($language));

    $this->languageManager->expects($this->any())
      ->method('getCurrentLanguage')
      ->will($this->returnValue($language));
  }
}

Test 1

  /**
   * Tests the children() method with an invalid key.
   *
   * @covers ::sendMailMessages
   */
  public function testInvalidRecipient() {
    $message = $this->getMock('\Drupal\contact\MessageInterface');
    $message->expects($this->once())
      ->method('isPersonal')
      ->willReturn(TRUE);

    $sender = $this->getMock('\Drupal\Core\Session\AccountInterface');
    $this->userStorage->expects($this->any())
      ->method('load')
      ->willReturn($sender);

    $this->setExpectedException(MailHandlerException::class, 'Unable to determine message recipient');

    $this->contactMailHandler->sendMailMessages($message, $sender);
  }

Test 2

 /**
   * Tests the sendMailMessages method.
   *
   * @dataProvider getSendMailMessages
   *
   * @covers ::sendMailMessages
   */
  public function testSendMailMessages(MessageInterface $message, AccountInterface $sender, $results) {
    $this->logger->expects($this->once())
      ->method('notice');
    $this->mailManager->expects($this->any())
      ->method('mail')
      ->willReturnCallback(
        function($module, $key, $to, $langcode, $params, $from) use (&$results) {
          $result = array_shift($results);
          $this->assertEquals($module, $result['module']);
          $this->assertEquals($key, $result['key']);
          $this->assertEquals($to, $result['to']);
          $this->assertEquals($langcode, $result['langcode']);
          $this->assertArrayEquals($params, $result['params']);
          $this->assertEquals($from, $result['from']);
        });
    $this->userStorage->expects($this->any())
      ->method('load')
      ->willReturn(clone $sender);
    $this->contactMailHandler->sendMailMessages($message, $sender);
  }

Test 2 - DataProvider

/**
   * Data provider for ::testSendMailMessages.
   */
  public function getSendMailMessages() {
    $message = $this->getAnonymousMockMessage($recipients, '');
    $sender = $this->getMockSender();
    $result = [
      'key' => 'page_mail',
      'params' => [
        'contact_message' => $message,
        'sender' => $sender,
        'contact_form' => $message->getContactForm(),
      ],
    ];
    $results[] = $result + $default_result;
    $data[] = [$message, $sender, $results];
    ...
    ...
    return $data;
  }

MockBuilder vs Prophesize

    // Create a route provider stub.
    $provider = $this->getMockBuilder(RouteProvider::class)
      ->disableOriginalConstructor()
      ->getMock();

    $provider->expects($this->any())
      ->method('getRouteByName')
      ->will($this->returnValueMap($route_name_return_map));

    $this->container = new ContainerBuilder();
    $this->container->set('router.route_provider', $provider);
    // Create a route provider stub.
    $provider =$this->prophesize(RouteProvider::class);

    $provider->getRouteByName()->willReturn($route_name_return_map);

    $this->container = new ContainerBuilder();
    $this->container->set('router.route_provider', $provider->reveal());

Refer: https://www.drupal.org/docs/8/phpunit/comparison-with-phpunit-mocks

Summary

  • Method level testing
  • Easy to add multiple data set (@dataProvider)
  • Fastest test runs.

2.2 Kernel tests

Drupal\KernelTests\KernelTestBase

Message Entity

/**
 * Defines the contact message entity.
 *
 * @ContentEntityType(
 *   id = "contact_message",
 *   label = @Translation("Contact message"),
 *   handlers = {
 *     "access" = "Drupal\contact\ContactMessageAccessControlHandler",
 *     "storage" = "Drupal\Core\Entity\ContentEntityNullStorage",
 *     "view_builder" = "Drupal\contact\MessageViewBuilder",
 *     "form" = {
 *       "default" = "Drupal\contact\MessageForm"
 *     }
 *   },
 *   admin_permission = "administer contact forms",
 *   entity_keys = {
 *     "bundle" = "contact_form",
 *     "uuid" = "uuid",
 *     "langcode" = "langcode"
 *   },
 *   bundle_entity_type = "contact_form",
 *   field_ui_base_route = "entity.contact_form.edit_form",
 * )
 */
class Message extends ContentEntityBase implements MessageInterface {

Setup

class MessageEntityTest extends EntityKernelTestBase {

  public static $modules = [
    'system',
    'contact',
    'field',
    'user',
    'contact_test',
  ];

  protected function setUp() {
    parent::setUp();
    $this->installConfig(['contact', 'contact_test']);
  }
  • installSchema - DB tables
  • installConfig - config/install
  • installEntitySchema - Storage for entities

 

Test 1

  public function testMessageMethods() {

    $message_storage = $this->container->get('entity.manager')->getStorage('contact_message');
    $message = $message_storage->create(['contact_form' => 'feedback']);

    // Check for empty values first.
    $this->assertEquals($message->getMessage(), '');
    $this->assertEquals($message->getSenderName(), '');

    // Set some values and check for them afterwards.
    $message->setMessage('welcome_message');
    $message->setSenderName('sender_name');

    // Check set values.
    $this->assertEquals($message->getMessage(), 'welcome_message');
    $this->assertEquals($message->getSenderName(), 'sender_name');

Summary

  • Functional integration testing
  • Minimal/on-demand bootstrap
  • Prefilled service container & DB
  • Much faster compared to Browser test

Background

Behat

(Gherkin syntax)

Mink

(Browser emulator)

Driver

Goutte

Zombie

PhantomJS

Sahi

E

C

E

Zombie.JS

(Node.js)

PhantomJS

(JavaScript)

Guzzle

(PHP)

C

Sahi

(Java)

Ctrl / Emulator

2.3 Function Tests

Drupal\Tests\BrowserTestBase

TestBase

Mink

(Browser emulator)

Drivers

Goutte

E

Guzzle

(PHP )

Ctrl / Emulator

HEADLESS BEHAT!

Setup

class ContactSitewideTest extends BrowserTestBase {

  use FieldUiTestTrait;
  use AssertMailTrait;

  public static $modules = [
    'text',
    'contact',
    'field_ui',
    'contact_test',
    'block',
    'error_service_test',
    'dblog',
  ];

  protected function setUp() {
    parent::setUp();
    $this->drupalPlaceBlock('system_breadcrumb_block');
    $this->drupalPlaceBlock('local_actions_block');
    $this->drupalPlaceBlock('page_title_block');
  }
}

Test 1

  public function testSiteWideContact() {
    // Create and log in administrative user.
    $admin_user = $this->drupalCreateUser([
      'access site-wide contact form',
      ...
      ...
    ]);
    $this->drupalLogin($admin_user);

    // Check the presence of expected cache tags.
    $this->drupalGet('contact');
    $this->assertCacheTag('config:contact.settings');

    $edit['contact_default_status'] = TRUE;
    $this->drupalPostForm('admin/config/people/accounts', $edit, t('Save configuration'));

    $this->drupalGet('admin/structure/contact');
    // Default form exists.
    $this->assertLinkByHref('admin/structure/contact/manage/feedback/delete');

Summary

  • Run in fresh installation
  • Full bootstrap
  • Less code to cover most of functional elements

2.4 Function Javascript Tests

Drupal\FunctionalJavascriptTests\JavascriptTestBase

TestBase

Mink

(Browser emulator)

Drivers

PhantomJS

C

PhantomJS

(JavaScript)

Ctrl / Emulator

Setup

use Drupal\FunctionalJavascriptTests\JavascriptTestBase;

/**
 * Tests for the machine name field.
 *
 * @group field
 */
class MachineNameTest extends JavascriptTestBase {

  public static $modules = ['node', 'form_test'];

  protected function setUp() {
    parent::setUp();

    $account = $this->drupalCreateUser([
      'access content',
    ]);
    $this->drupalLogin($account);
  }


}

Test

  public function testMachineName() {

    // Visit the machine name test page which contains two machine name fields.
    $this->drupalGet('form-test/machine-name');

    // Get page and session.
    $page = $this->getSession()->getPage();

    $title = $page->findField('machine_name_1_label');
    $machine_name_field = $page->findField('machine_name_1');

    $machine_name_wrapper = $machine_name_field->getParent();
    $machine_name_value = $page->find('css', '#edit-machine-name-1-label-machine-name-suffix .machine-name-value');

    // Assert field is initialized correctly.
    $this->assertNotEmpty($machine_name_value, 'Machine name field must be initialized');

    // Set the value for the field, triggering the machine name update.
    $title->setValue('Test value !0-9@');
 
   // Wait the set timeout for fetching the machine name.
   $this->assertJsCondition('jQuery("#edit-machine-name-1-label-machine-name-suffix .machine-name-value").html() == "test_value_0_9_"');

   // Validate the generated machine name.
   $this->assertEquals('test_value_0_9_', $machine_name_value->getHtml(), $test_info['message']);

Summary

  • Tests browser level interactions
  • Needs PhantomJS
  • Generates Screenshots!

2.5 Behat Tests

Behat

(Gherkin syntax)

Mink

(Browser emulator)

Driver

Goutte

Drush

Drupal

PhantomJS

E

E

Drush

(PHP)

Drupal API

(PHP)

Guzzle

(PHP )

E

PhantomJS

(JavaScript)

Ctrl / Emulator

E

1. behat.yml

default:
  suites:
    contact:
      contexts:
        - DrupalCoreExtension\Context\DrupalContext
        - Drupal\DrupalExtension\Context\ConfigContext
        - Drupal\DrupalExtension\Context\DrupalContext
        - Drupal\DrupalExtension\Context\MinkContext
        - Drupal\DrupalExtension\Context\MarkupContext
        - Drupal\DrupalExtension\Context\MessageContext
      filters:
        tags: "@contact"
  extensions:
    Behat\MinkExtension:
      base_url: http://localhost/
      sessions:
        default:
          goutte: ~
    Drupal\DrupalExtension:
      api_driver: "drupal"
      drupal:
        drupal_root: "/www/htdocs/docroot"
      region_map:
        left sidebar: "#sidebar-first"
        content: "#content"
      selectors:
        error_message_selector: '.messages--error'

2. Feature

@d8 @api @contact
Feature: Contact
  In order to prove the Drupal contact module is working properly for Drupal 8
  As a site administrator
  I need to check end user and contact form administrator functionality.

  Scenario: Visit feedback form as a logged in user.
    Given I am logged in as a user with the "authenticated user" role
    When I am on "/contact"
    Then I should see the text "Website feedback"
    And I should see the text "Your name"
    And I should see the text "Your email address"
    And I should see the text "Subject"
    And I should see the text "Message"
    And I should see the text "Send yourself a copy"
    And I should not see "name" field
    And I should not see "mail" field
    And I should see "subject[0][value]" field
    And I should see "message[0][value]" field
    And I should see "Preview" button
    And I should see "Send message" button

  Scenario: Visit feedback form as a anonymous user.
    Given I am an anonymous user
    When I am on "/contact"
    Then I should see the text "Website feedback"
    And I should see the text "Your name"
    And I should see the text "Your email address"
    And I should see the text "Subject"
    And I should see the text "Message"
    And I should not see the text "Send yourself a copy"
    And I should see "name" field
    And I should see "mail" field
    And I should see "subject[0][value]" field
    And I should see "message[0][value]" field
    And I should see "Preview" button
    And I should see "Send message" button

  Scenario: Visit contact form admin pages as administrator.
    Given I am logged in as a user with the "administer contact forms" permission
    When I am on "/admin/structure/contact"
    Then I should see the heading "Contact forms"
    Then I should see the text "Personal contact form"

3. Context

  /**
   * Creates and authenticates a user with the given role(s).
   *
   * @Given I am logged in as a user with the :role role(s)
   * @Given I am logged in as a/an :role
   */
  public function assertAuthenticatedByRole($role) {
    ...
  }

  /**
   * Opens specified page.
   *
   * Example: Given I am on "http://batman.com"
   * Example: And I am on "/articles/isBatmanBruceWayne"
   * Example: When I go to "/articles/isBatmanBruceWayne"
   *
   * @Given /^(?:|I )am on "(?P<page>[^"]+)"$/
   * @When /^(?:|I )go to "(?P<page>[^"]+)"$/
   */
  public function visit($page) {
    ...
  }

  /**
   * Checks, that element with specified CSS exists on page.
   *
   * Example: Then I should see a "body" field
   * Example: And I should see a "body" field
   *
   * @Then /^(?:|I )should see an? "(?P<field>[^"]*)" field$/
   */
  public function assertElementOnPage($field) {
    ...
  }

4. Extension

Drupal Core Extension

5. Output

3. Lessons learned

Code

  • Not public property - public vs private vs protected
  • No utility class/service
  • Write code -> write test -> refactor -> repeat

Run Tests

$ phantomjs --ssl-protocol=any --ignore-ssl-errors=true vendor/jcalderonzumba/gastonjs/src/Client/main.js
   


$ php core/scripts/run-tests.sh --verbose  --url http://d8 --class "Drupal\Tests\permission_ui\Unit\Menu\PermissionUiLocalTasksTest"

Annotations

  • @group
  • @runTestsInSeparateProcesses
  • @preserveGlobalState

References

Thank you!

Drupal 8 Testing

By Vijay Chandran Mani

Drupal 8 Testing

Automated testing framework available in Drupal 8 core and integration with external frame works.

  • 1,394