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
-
Unit
-
Kernel
-
Functional
-
Functional Javascript
-
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



