DEV Community

pamlesleylu
pamlesleylu

Posted on

3 2

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

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more