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.
- Needs a very small installation
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
- Mocking Drupal by @mattkineme
-
Drupal Examples Module
- Simpletest example
- PHPUnit example
- DrupalCamp Spain Granada 2016
- DrupalCon Dublin 2016:
- Drupal Core code
Questions?
Writing testable Code in Drupal 8
By jlbellido
Writing testable Code in Drupal 8
Session given during the DrupalDay Santiago de Compostela 2016
- 3,353