pamlesleylu
Posted on November 22, 2020
Recently, I was tasked to create a unit test for a custom Configuration Form that we created in Drupal 8. As I am fairly new to Drupal 8, I had to find online resources that can give more details on this.
Although the official online Drupal website has pages dedicated to Drupal 8 testing, I still find it a bit lacking as it was not too beginner-friendly for me. I tried looking for other resources but couldn't find any that are simple and specific to my needs.
After a bit of trial-and-errors, I finally have a basic set-up for unit testing a form within a custom module. I have decided to document this set-up here in the hopes that it might be able to help another newbie like me.
Set up PHPUnit
Drupal 8 uses PHPUnit to run its unit tests and it should already be available as a dependency when you first set up Drupal 8 via composer. A basic PHPUnit configuration can be obtained from copying the core's phpunit.xml.dist
file (found in web/core/phpunit.xml.dist
) into your root directory and renaming it to phpunit.xml
.
For my case, I only want to execute unit tests for my custom modules so I modified some lines in the configuration file. The configuration file that I came up with is as follows (check the comments for the details of the changes):
<?xml version="1.0" encoding="UTF-8"?>
<!-- Changed bootstrap to 'web/core/tests/bootstrap.php' as my Drupal core is located in web/core
-->
<phpunit bootstrap="web/core/tests/bootstrap.php" colors="true" beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutOutputDuringTests="true" beStrictAboutChangesToGlobalState="true" printerClass="\Drupal\Tests\Listeners\HtmlOutputPrinter">
<php>
<!-- Set error reporting to E_ALL. -->
<ini name="error_reporting" value="32767"/>
<!-- Do not limit the amount of memory tests take to run. -->
<ini name="memory_limit" value="-1"/>
</php>
<testsuites>
<!-- Add a testsuite for the custom module -->
<!-- To execute, run `vendor/bin/phpunit --testsuite=module-name` -->
<testsuite name="module-name">
<!-- Tests should be placed inside the `tests` directory within the module --> <directory>./web/modules/custom/module_name/tests/</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="\Drupal\Tests\Listeners\DrupalListener">
</listener>
<!-- The Symfony deprecation listener has to come after the Drupal listener -->
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
</listener>
</listeners>
<!-- Filter for coverage reports. -->
<filter>
<whitelist>
<!-- Extensions can have their own test directories, so exclude those. -->
<directory>./web/modules/custom/module_name</directory>
<exclude>
<directory>./web/modules/custom/module_name/*/tests</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
Creating a Unit Test
There are some conventions that must be followed when creating a unit test. Some of them are
- Unit tests for a custom modules must be in a
tests
subdirectory within the custom module directory - Unit tests must be placed inside
src/Unit
(e.g.,modules/custom/module_name/tests/src/Unit/
) - Class name of test must end in
Test
(e.g.,CustomModuleFormTest
- Unit test must extend from
Drupal\Tests\UnitTestCase
- Test methods must start with
test
(e.g.,testCalculateSum()
) - A mocking library (Prophecy) is already available and can be used via
$this->prophesize
The code below is the very simple unit test that I have created that uses the aforementioned conventions. You may check the embedded comments for the details.
<?php
// modules/custom/module_name/tests/src/Unit/CustomModuleFormTest.php
// Namespace must follow Drupal\Tests\module_name\Unit
namespace Drupal\Tests\module_name\Unit;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Config;
use Drupal\Core\Form\FormState;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\savings_calculator\Form\SavingsCalculatorSettingsForm;
use Drupal\Tests\UnitTestCase;
/**
* Test class for CustomModuleForm
*
* Must extend from UnitTestCase
*/
class CustomModuleFormTest extends UnitTestCase {
/**
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
private $translationInterfaceMock;
/**
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
private $configFactoryMock;
/**
* @var \Drupal\Core\Config\Config
*/
private $configMock;
/**
* @var \Drupal\savings_calculator\Form
*/
private $form;
public function setUp() {
// prophesize() is made available via extension from UnitTestCase
// Call this method to create a mock based on a class
$this->translationInterfaceMock = $this->prophesize(TranslationInterface::class);
// Create mock to return config that will be used in the code under test
$this->configMock = $this->prophesize(Config::class);
// When the get method of the config is called with the parameter 'custom_property',
// the array ['label' => 'Discounts'] will be returned.
$this->configMock->get('custom_property')->willReturn([
'label' => 'Discounts'
]);
$this->configFactoryMock = $this->prophesize(ConfigFactoryInterface::class);
$this->configFactoryMock->getEditable('module_name.settings')->willReturn($this->configMock);
// Instantiate the code under test
$this->form = new CustomModuleForm($this->configFactoryMock->reveal());
// Config Base Form has a call to $this->t() which references the TranslationService
// Set the translation service mock so that the program won't throw an error
$this->form->setStringTranslation($this->translationInterfaceMock->reveal());
}
// Test that the correct form ID is returned
public function testFormId() {
$this->assertEquals('module_name_settings_form', $this->form->getFormId());
}
// Test that the correct form fields are added
public function testBuildForm() {
// Arrange
$form = [];
$form_state = new FormState();
// Act
// Call the function being tested
$retForm = $this->form->buildForm($form, $form_state);
// Assert
$this->assertEquals('module_theme', $retForm['#theme']);
$this->assertArrayEquals(['#type' => 'submit'], $retForm['submit']);
// The code under test retrieves the label from config
// Check that the label returned by config is given
// to the title attribute of the custom field
$this->assertArrayEquals(
[
'#type' => 'textfield',
'#title' => 'Discounts'
],
$retForm['custom']
);
}
}
Running the Unit Test
To run the unit, simply execute vendor/bin/phpunit --testsuite=module-name
References
Posted on November 22, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.