DEV Community

pamlesleylu
pamlesleylu

Posted on

Drupal 8 Unit Tests for Custom Forms

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> 
Enter fullscreen mode Exit fullscreen mode

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'] ); } } 
Enter fullscreen mode Exit fullscreen mode

Running the Unit Test

To run the unit, simply execute vendor/bin/phpunit --testsuite=module-name

References

Top comments (0)