Specbee: How to Write Your First Test Case Using PHPUnit & Kernel in Drupal

Are you able to imagine a world where your code functions flawlessly, your bugs are scared of your testing routine and your users can enjoy a seamless experience - free from crashes and errors? Well, this only means that you understand the importance of automated testing.  With automated testing, Drupal developers can elevate the code quality, streamline workflows, and fortify digital ecosystems against errors and bugs. Drupal offers 4 types of PHPUnit tests: Unit, Kernel, Functional, and Functional Javascript. In this blog post, we'll explore PHPUnit tests and Kernel tests. Setting Up PHPUnit in Drupal For setting up PHPUnit in Drupal, the recommended method by Drupal is composer-based: $ composer require --dev phpunit/phpunit --with-dependencies $ composer require behat/mink && composer require --dev phpspec/prophecy(Note  -  PHPUnit version 11 requires PHP 8.2, This blog is written for Drupal 10 and PHP 8.1)Once PHPUnit and its dependencies are installed, the next step involves creating and configuring the phpunit.xml file. Locate the phpunit.xml.dist file in the core folder of Drupal installation. Copy and paste this file into the docroot directory and rename it to phpunit.xml (it is recommended to keep the file in docroot directory instead of core so that it won't get affected by core updates). Create simpletest and browser_output directories. In order to run tests like Kernel and Functional we need to create these two directories and set the permissions to writable $ mkdir -p docroot/sites/simpletest/browser_output && chmod -R 777 docroot/sites/simpletestTo run the test locally, we need to configure some values in phpunit.xml e.g, Change 1 - <env name="SIMPLETEST_BASE_URL" value="" /> to <env name="SIMPLETEST_BASE_URL" value="https://yoursiteurl.com" /> 2 - <env name="SIMPLETEST_DB" value="" /> to <env name="SIMPLETEST_DB" value="mysql://username:password@yourdbhost/databasename" /> 3 - <env name="BROWSERTEST_OUTPUT_DIRECTORY" value="" /> to <env name="BROWSERTEST_OUTPUT_DIRECTORY" value="fullpath/docroot/sites/simpletest/browser_output" /> (Note - To check the full path of your app run from project root) $ pwdSetting Up PHPUnit with Lando If you want to run your test from lando you’ll need to configure .lando.yml file. We’ll leave the defaults for most of the values in the phpunit.xml file except for where to find the bootstrap.php file. This should be changed to the path in the Lando container, which will be /app/web/core/tests/bootstrap.php. This can be done with sed: $ sed -i 's|tests\/bootstrap\.php|/app/web/core/tests/bootstrap.php|g' phpunit.xml Next, edit the .lando.yml file and add the following: services:  appserver:    overrides:      environment:        SIMPLETEST_BASE_URL: "http://mysite.lndo.site"        SIMPLETEST_DB: "mysql://database:database@database/database"        MINK_DRIVER_ARGS_WEBDRIVER: '["chrome", {"browserName":"chrome","chromeOptions":{"args":["--disable-gpu","--headless"]}}, "http://chrome:9515"]'  chrome:    type: compose    services:      image: drupalci/webdriver-chromedriver:production      command: chromedriver --log-path=/tmp/chromedriver.log --verbose --whitelisted-ips= tooling:  test:    service: appserver    cmd: "php /app/vendor/bin/phpunit -c /app/phpunit.xml"Modify SIMPLETEST_BASE_URL and SIMPLETEST_DB to point to your lando site and database credentials as needed.This does three things: Adds environment variables to the appserver container (the one we’ll run the tests in). Adds a new container for the chromedriver image which is used for running headless javascript tests (more on that later). A tooling section that adds a test command to Lando to run our tests. Important!After updating the .lando.yml file we need to rebuild the containers with the following:$ lando rebuild -yLets run a single test with lando $ lando test core/modules/datetime/tests/src/Unit/Plugin/migrate/field/DateFiedTest.phpWithout Lando — Verify the tests are working by running core test.(Note — I keep my phpunit.xml file in docroot folder and will be running test from docroot directory.)$ ../vendor/bin/phpunit -c core core/modules/datetime/tests/src/Unit/Plugin/migrate/field/DateFiedTest.php What is a PHPUnit Test PHPUnit tests are utilized to test small blocks of code and functionalities that do not necessitate a complete Drupal installation. These tests allow us to evaluate the functionality of a class within the Drupal environment, encompassing aspects like Database, Settings, etc. Moreover, they do not require a web browser, as the Drupal environment can be substituted by a "mock" object. Before writing unit tests, we should remember the following things: Base Class — \Drupal\Tests\UnitTestCaseTo implement a unit test case we need to extend our test class with the base classNamespace — \Drupal\Tests\mymodule\Unit (or subdirectory)We need to specify a namespace for our testDirectory location — mymodule/tests/src/Unit (or subdirectory)To run the test, the test class must reside in the above-mentioned directory. Write Your First PHPUnit Test Step 1: Create a custom module Step 2: Create the event_example.info.yml file for the custom module name: Events Exampletype: moduledescription: Provides an example of subscribing to and dispatching events.package: Customcore_version_requirement: ^9.4 || ^10 Step 3: Create event_example.services.yml file services: # Give your service a unique name, convention is to prefix service names with # the name of the module that implements them. events_example_subscriber:  # Point to the class that will contain your implementation of  # \Symfony\Component\EventDispatcher\EventSubscriberInterface  class: Drupal\events_example\EventSubscriber\EventsExampleSubscriber  tags:  - {name: event_subscriber}Step 4: Create events_example/src/EventSubscriber/EvensExampleSubscriber.php file for our class. <?php namespace Drupal\events_example\EventSubscriber; use Drupal\events_example\Event\IncidentEvents; use Drupal\events_example\Event\IncidentReportEvent; use Drupal\Core\Messenger\MessengerTrait; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Subscribe to IncidentEvents::NEW_REPORT events and react to new reports. * * In this example we subscribe to all IncidentEvents::NEW_REPORT events and * point to two different methods to execute when the event is triggered. In * each method we have some custom logic that determines if we want to react to * the event by examining the event object, and the displaying a message to the * user indicating whether or not that method reacted to the event. * * By convention, classes subscribing to an event live in the * Drupal/{module_name}/EventSubscriber namespace. * * @ingroup events_example */ class EventsExampleSubscriber implements EventSubscriberInterface {  use StringTranslationTrait;  use MessengerTrait;  /**   * {@inheritdoc}   */  public static function getSubscribedEvents() {    // Return an array of events that you want to subscribe to mapped to the    // method on this class that you would like called whenever the event is    // triggered. A single class can subscribe to any number of events. For    // organization purposes it's a good idea to create a new class for each    // unique task/concept rather than just creating a catch-all class for all    // event subscriptions.    //    // See EventSubscriberInterface::getSubscribedEvents() for an explanation    // of the array's format.    //    // The array key is the name of the event your want to subscribe to. Best    // practice is to use the constant that represents the event as defined by    // the code responsible for dispatching the event. This way, if, for    // example, the string name of an event changes your code will continue to    // work. You can get a list of event constants for all events triggered by    // core here:    // https://api.drupal.org/api/drupal/core%21core.api.php/group/events/8.2.x.    //    // Since any module can define and trigger new events there may be    // additional events available in your application. Look for classes with    // the special @Event docblock indicator to discover other events.    //    // For each event key define an array of arrays composed of the method names    // to call and optional priorities. The method name here refers to a method    // on this class to call whenever the event is triggered.    $events[IncidentEvents::NEW_REPORT][] = ['notifyMario'];    // Subscribers can optionally set a priority. If more than one subscriber is    // listening to an event when it is triggered they will be executed in order    // of priority. If no priority is set the default is 0.    $events[IncidentEvents::NEW_REPORT][] = ['notifyBatman', -100];    // We'll set an event listener with a very low priority to catch incident    // types not yet defined. In practice, this will be the 'cat' incident.    $events[IncidentEvents::NEW_REPORT][] = ['notifyDefault', -255];    return $events;  }  /**   * If this incident is about a missing princess, notify Mario.   *   * Per our configuration above, this method is called whenever the   * IncidentEvents::NEW_REPORT event is dispatched. This method is where you   * place any custom logic that you want to perform when the specific event is   * triggered.   *   * These responder methods receive an event object as their argument. The   * event object is usually, but not always, specific to the event being   * triggered and contains data about application state and configuration   * relative to what was happening when the event was triggered.   *   * For example, when responding to an event triggered by saving a   * configuration change you'll get an event object that contains the relevant   * configuration object.   *   * @param \Drupal\events_example\Event\IncidentReportEvent $event   *   The event object containing the incident report.   */  public function notifyMario(IncidentReportEvent $event) {    // You can use the event object to access information about the event passed    // along by the event dispatcher.    if ($event->getType() == 'stolen_princess') {      $this->messenger()->addStatus($this->t('Mario has been alerted. Thank you. This message was set by an event subscriber. See @method()', ['@method' => __METHOD__]));      // Optionally use the event object to stop propagation.      // If there are other subscribers that have not been called yet this will      // cause them to be skipped.      $event->stopPropagation();    }  }  /**   * Let Batman know about any events involving the Joker.   *   * @param \Drupal\events_example\Event\IncidentReportEvent $event   *   The event object containing the incident report.   */  public function notifyBatman(IncidentReportEvent $event) {    if ($event->getType() == 'joker') {      $this->messenger()->addStatus($this->t('Batman has been alerted. Thank you. This message was set by an event subscriber. See @method()', ['@method' => __METHOD__]));      $event->stopPropagation();    }  }  /**   * Handle incidents not handled by the other handlers.   *   * @param \Drupal\events_example\Event\IncidentReportEvent $event   *   The event object containing the incident report.   */  public function notifyDefault(IncidentReportEvent $event) {    $this->messenger()->addStatus($this->t('Thank you for reporting this incident. This message was set by an event subscriber. See @method()', ['@method' => __METHOD__]));    $event->stopPropagation();  }  /**   * @param $string String   *   Simple function to check string.   */  public function checkString($string) {    return $string ? TRUE : FALSE;  } }Step 5: Create tests/src/Unit/EventsExampleUnitTest.php file for our UnitTest <?php namespace Drupal\Tests\events_example\Unit; use Drupal\Tests\UnitTestCase; use Drupal\events_example\EventSubscriber\EventsExampleSubscriber; /** * Test events_example EventsExampleSubscriber functionality * * @group events_example * * @ingroup events_example */ class EventsExampleUnitTest extends UnitTestCase {    /**     * event_example EventsExampleSubscriber object.     *     * @var-object     */    protected $eventExampleSubscriber;    /**     * {@inheritdoc}     */     protected function setUp(): void {        $this->eventExampleSubscriber = new EventsExampleSubscriber();        parent::setUp();     }    /**     * Test simple function that returns true if string is present.     */    public function testHasString() {      $this->assertEqual(TRUE, $this->eventExampleSubscriber->checkString('Some String'));    }  }Important Notes: Test class name should start or end with “Test”. For example, EventsExampleUnitTest and it should extend with the base class UnitTestCase. Test function should start with “test”. For example, testHasString otherwise it will not be included in test run. Step 6: Let’s run our test! $ lando test modules/custom/events_example/tests/src/Unit/EventsExampleUnitTest.phpor$ ../vender/bin/phpunit -c core modules/custom/events_example/tests/src/Unit/EventsExampleUnitTest.php PHPUnit 9.6.17 by Sebastian Bergmann and contributors. Testing Drupal\Tests\events_example\Unit\EventsExampleUnitTest.                                                                   1 / 1 (100%) Time: 00:00.054, Memory: 10.00 MB OK (1 test, 1 assertion) Hooray! Our test has passed! Write Your First Kernel Test These tests necessitate specific Drupal environment dependencies, with no requirement for web browsers. They allow us to assess class functionality without the full Drupal setup or web browsers. However, certain Drupal environment dependencies are indispensable and cannot be easily mocked. Kernel tests are capable of accessing services, databases, and minimal file systems.Below are the essential prerequisites to consider when running kernel tests. Base Class — \Drupal\KernelTests\KernelTestBaseTo implement the Kernel test we need to extend our test class with the base classNamespace — \Drupal\Tests\mymodule\Kernel (or subdirectory)We need to specify a namespace for our testDirectory location — mymodule/tests/src/Kernel (or subdirectory)To run the test, the test class must reside in the above-mentioned directory.Create tests/src/Kernel/EventsExampleServiceTest.php file for our Kernel Test <?php namespace Drupal\Tests\events_example\Kernel; use Drupal\KernelTests\KernelTestBase; use Drupal\events_example\EventSubscriber\EventsExampleSubscriber; /** * Test to ensure 'events_example_subscriber' service is reachable. * * @group events_example * * @ingroup events_example */ class EventsExampleServiceTest extends KernelTestBase {  /**   * {@inheritdoc}   */  protected static $modules = ['events_example'];  /**   * Test for existence of 'events_example_subscriber' service.   */  public function testEventsExampleService() {    $subscriber = $this->container->get('events_example_subscriber');    $this->assertInstanceOf(EventsExampleSubscriber::class, $subscriber);  } }Important Notes: The test class name should start or end with “Test”, for example, EventsExampleServiceTest and it should extend with base class KernelTestBase.  The kernel test should include modules that have dependencies. For example , “$modules = [‘events_example’]” we can include other dependent modules here like “$modules = [‘events_example’, ‘user’, ‘field’]” . It acts like dependency injection. The test function should start with “test”, for example, testEventsExampleService otherwise it will not be included in the test run.Let's run our Kernel Test.$ lando test modules/custom/events_example/tests/src/Kernel/EventsExampleServiceTest.phpor$ ../vender/bin/phpunit -c core modules/custom/events_example/tests/src/Kernel/EventsExampleServiceTest.php PHPUnit 9.6.17 by Sebastian Bergmann and contributors. Testing Drupal\Tests\events_example\Kernel\EventsExampleServiceTest.                                                                   1 / 1 (100%) Time: 01:24.129, Memory: 10.00 MB OK (1 test, 1 assertion) Woohoo! Another successful test in the books! Final Thoughts What next after writing your first test case using PHPUnit and Kernel testing? Well, you can proceed to write more test cases to cover other functionalities or edge cases in your Drupal project. Additionally, you may want to consider integrating your test suite into a continuous integration(CI) pipeline to automate testing and ensure code quality throughout your development process.
PubDate

Tags