Writing Testable code in Drupal 8. 

DrupalDay Santiago de Compostela 2016

Jose Luis Bellido (jlbellido)

@jose_lakatos

https://github.com/jlbellido

https://www.drupal.org/u/jlbellido

Why this session?

During this session

  • Introduction
    • Some basic concepts
    • Improvements from Drupal 7
    • Testing in Drupal 8
    • What's going on?
  • Writing "no testable" code
    • Code not  testable by Unit tests.
  • Writing testable code.
    • Code testable by Unit tests

Some Concepts..

Unit Testing

  • Test isolated specific pieces of code (Units).
  • Is not needed a Drupal installation.
  • We need to Mock our dependencies.

Some Concepts..

Integration Testing

  • Test the interaction between different modules.
  • In case of Drupal:
    • Needs a very small installation
      • You have to specify the config needed.
    • Much less heavy than functional tests.

Some Concepts..

Functional Testing

  • Feeds the input and examine the output
  • Internal program structures doesn't matter.
  • In case of Drupal:
    • Needs a drupal installation.
    • Based on Simpletest.

Introduction

Testing In Drupal 7

  • Mostly procedure Code
  • All using Simpletest
    • Functional tests
    • Unit tests
  • Upgrade tests
  • Not easy to have non functional tests.
  • Almost everything using functional tests.
  • Avoid using Simpletest.
  • Run all tests takes a lot..
  • Not using common accepted tools like PHPUnit.

The effects...

Improvements in Drupal 8 (I)

  • Still need the simpletest module
  • Much less number of tests in Core.
    • Just the really needed!
  • Extending form the class WebTestBase
  • Also you can extend from:
    • NodeTestBase
    • BlockTestBase
    • CommentTestBase
    • ...

Functional testing

/**
   * Tests the behavior when there are no actions to list in the admin page.
   */
  public function testEmptyActionList() {
    // Create a user with permission to view the actions administration pages.
    $this->drupalLogin($this->drupalCreateUser(['administer actions']));

    // Ensure the empty text appears on the action list page.
    /** @var $storage \Drupal\Core\Entity\EntityStorageInterface */
    $storage = $this->container->get('entity.manager')->getStorage('action');
    $actions  = $storage->loadMultiple();
    $storage->delete($actions);
    $this->drupalGet('/admin/config/system/actions');
    $this->assertRaw('There is no Action yet.');
  }

core/modules/action/src/Tests/ActionListTest.php

Improvements in Drupal 8 (I)

Functional testing

Improvements in Drupal 8 (II)

  • New in Drupal 8!!!
  • Extending KernelTestBase.php class
  • You can run them with PHPUnit
  • Tests can access to the Database and Files.
  • Runs in a minimal environment.
  • The module/hook system is functional.
  • Modules are only loaded, not installed.

 

Integration testing

Improvements in Drupal 8 (II)

Integration testing Example

/**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();
    $this->installSchema('system', 'sequences');
    $this->installSchema('node', 'node_access');
    $this->installEntitySchema('user');
    $this->installEntitySchema('node');
    $this->installConfig('filter');
    $this->installConfig('node');
    $this->accessHandler = $this->container->get('entity_type.manager')
      ->getAccessControlHandler('node');
    // Clear permissions for authenticated users.
    $this->config('user.role.' . RoleInterface::AUTHENTICATED_ID)
      ->set('permissions', [])
      ->save();

    // Create user 1 who has special permissions.
    $this->drupalCreateUser();

    // Create a node type.
    $this->drupalCreateContentType(array(
      'type' => 'page',
      'name' => 'Basic page',
      'display_submitted' => FALSE,
    ));
  }

Improvements in Drupal 8 (III)

  • Based on PHPUnit
  • Extending  UnitTestCase Class
  • You can run them without using run-tests.sh
  • Mocking Drupal is hard.
    • This is the funny part.

 

Unit testing

Improvements in Drupal 8 (III)

Mocking with PHPUnit

    $entity_storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
    $entity_storage->expects($this->any())
      ->method('loadMultiple')
      ->will($this->returnValue($actions));   

Improvements in Drupal 8 (III)

Mocking with prophecy

    // Mock a user entity:
    $mock_user = $this->prophesize(User::class);
    $mock_user->getAccountName()->willReturn('example username');

What is Going on ?

The goal: Remove Simpletest from Core and use PHPUnit as test runner.

New class BrowserTestBase.php 

Now happening: 

Writing No Testable Code

Requirements:

Writing No Testable Code

Node must have a fixed text depending on their type. The patterns are as follows:

  • Basic pages: "Page: @title"
  • Articles: "Awesome article by @author".

First Solution: Procedural Code

Writing No Testable Code

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function d8_testable_code_example_node_presave(Drupal\Core\Entity\EntityInterface $entity) {
  // Updates the title to a fix string:
  // - Basic pages: "Page: @title"
  // - Articles: "Awesome article by @author".
  switch ($entity->bundle()) {
    case 'page':
      $generated_title = _d8_testable_code_page_title_generate($entity);
      break;
    case 'article':
      $generated_title = _d8_testable_code_article_title_generate($entity);
      break;
    default:
      $generated_title = $entity->getTitle();
  }

  $entity->setTitle($generated_title);
}

First Solution: Procedural Code

Writing No Testable Code

/**
 * Set the title "Page: @title" to the given Node.
 * @param \Drupal\node\NodeInterface $node
 * @return string
 */
function _d8_testable_code_page_title_generate(\Drupal\node\NodeInterface $node) {
  return (string) new FormattableMarkup('Page: @title', ['@title' => $node->getTitle()]);
}

/**
 * Set the title "Awesome article by @author" to the given Node.
 * @param \Drupal\node\NodeInterface $node
 * @return string
 */
function _d8_testable_code_article_title_generate(\Drupal\node\NodeInterface $node) {
  $author_name = $node->getOwner()->getAccountName();
  return (string) new FormattableMarkup('Awesome article by @author', ['@author' => $author_name]);
}

First Solution: Procedural Code

Writing No Testable Code

  • Usual code in all modules previously to D8.
  • Procedural code:
    • All in .module or .inc files
  • Can be tested with Functional or Integration tests.
  • We want to have it covered by Unit Tests!!!

We want to avoid the Drupal Inquisition!

And....

We want to avoid the Drupal Inquisition!

And....

Writing Testable Code

Writing Testable Code

Step 1: Move our code to a Service

  • Really easy:
    • drupal generate:service
  • Replace our custom functions to methods.

Writing Testable Code

Step 1: Move our code to a Service

services:
  d8_testable_code_example.node_title_generator:
    class: Drupal\d8_testable_code_example\NodeTitleGenerator
    arguments: []

d8_testable_code_example.services.yml

d8_testable_code_example.services.yml

/**
 * Class NodeTitleGenerator.
 *
 * @package Drupal\d8_testable_code_example
 */
class NodeTitleGenerator implements NodeTitleGeneratorInterface {

  /**
   * Generate and update the title of the given node depending on the bundle
   * as follows:
   * - Basic pages: "Page: @title"
   * - Articles: "Awesome article by @author".
   *
   * @param \Drupal\node\NodeInterface $node .
   * @return string
   */
  public function generateTitle(NodeInterface $node) {
    switch ($node->bundle()) {
      case 'page':
        $title = $this->generate_page_title($node);
        break;
      case 'article':
        $title = $this->generate_article_title($node);
        break;
      default:
        $title = $node->getTitle();
        break;
    }

    return (string)$title;
  }

Writing Testable Code

Step 2: use the seRvice inside THe Hook

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function d8_testable_code_example_node_presave(\Drupal\node\NodeInterface $node) {
  $generated_title = \Drupal::service('d8_testable_code_example.node_title_generator')
    ->generateTitle($node);
  $node->setTitle($generated_title);
}

Writing Testable Code

Step 3: Inject other services OR use Traits INSTEAD of Procedural functions

Writing Testable Code

The t() function : 

Use StringTranslationTratit.php

new TranslatableMarkup('Other', array(), array('context' => 'Entity type group'));

Use TranslatableMarkup() Class

abstract class FormBase implements FormInterface, ContainerInjectionInterface {

...
  use StringTranslationTrait;
...

Writing Testable Code

Inject Other services instead of \Drupal::service() 

Update the Module.services.yml

Inject it into the constructor.

services:
  d8_testable_code_example.node_title_generator:
    class: Drupal\d8_testable_code_example\NodeTitleGenerator
    arguments: ['@language_manager']
  /**
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $language_manager;

  public function __construct(LanguageManagerInterface $language_manager) {
    /**
     * Instead of usinf the language_manager service as: \Drupal::service('language_manager')
     * We inject it and from now we can use it as : $this->language_manager...
     */
    $this->language_manager = $language_manager;
  }

Writing Testable Code

Adding the unit tests

  public function testGeneratePageTitleTest() {
    // Mock a user entity:
    $mock_user = $this->prophesize(User::class);
    $mock_user->getAccountName()->willReturn('example username');

    // Mock the node entity:
    $mock_node = $this->prophesize(Node::class);
    $mock_node->getTitle()->willReturn('Title example');
    $mock_node->bundle()->willReturn('page');
    $mock_node->getOwner()->willReturn($mock_user->reveal());

    $page_node = $mock_node->reveal();
    $generated_title = $this->node_title_generator->generateTitle($page_node);
    $this->assertEquals('Page: Title example', $generated_title);

    $mock_node->bundle()->willReturn('article');
    $article_node = $mock_node->reveal();
    $generated_title = $this->node_title_generator->generateTitle($article_node);
    $this->assertEquals('Awesome article by example username', $generated_title);
  }

Writing Testable Code

Running the tests..

$ ../vendor/phpunit/phpunit/phpunit ../modules/custom/ --debug
PHPUnit 4.8.27 by Sebastian Bergmann and contributors.


Starting test 'Drupal\Tests\d8_testable_code_example\Kernel\NodeTitleGenerateKernelTest::testGeneratePageTitle'.
.
Starting test 'Drupal\Tests\d8_testable_code_example\Kernel\NodeTitleGenerateKernelTest::testGenerateArticleTitle'.
.
Starting test 'Drupal\Tests\d8_testable_code_example\Unit\NodeTitleGenerateUnitTest::testGeneratePageTitleTest'.
.

Time: 2.7 seconds, Memory: 6.00Mb

OK (3 tests, 10 assertions)

ConCLUSIONS

  • Cleaner & maintainable code.
  • Hard at the beginning if you are not used to.
  • Quick result tests.
  • Useful in other frameworks.

El Camino se hace caminando 

Useful Readings

Questions?

Writing testable Code in Drupal 8

By jlbellido

Writing testable Code in Drupal 8

Session given during the DrupalDay Santiago de Compostela 2016

  • 2,327
Loading comments...

More from jlbellido