Back to blogs

Setting up Kernel Testing in Drupal

Submitted on: January 8 2025

Introduction

Kernel tests in Drupal are crucial for ensuring the integrity and functionality of custom modules and their interaction with core subsystems. These tests validate integration between system components within a reduced Drupal environment, offering deeper coverage than unit tests without the complexity of full functional tests.

Note: 
You can also test services, controllers, specific functions of a service, among a lot of things, I clarify this because my original thought was that the original logic needed to be replicated in the test, and really the purpose is to be able to directly use the service, controller, function directly to test it in the kernel test. I have also noticed that hooks and event subscriber are really difficult to test, so I will investigate this and see if I am doing it wrong and if so, I will modify or do something detailed for those tests.

This document provides a detailed guide to setting up and running Kernel tests in Drupal using DDEV. We'll delve into technical concepts and provide practical examples to illustrate their importance and application in Drupal project development.


Steps to Configure Kernel Tests

1. Verify PHPUnit Installation

Before starting, it's essential to ensure that PHPUnit is installed in your development environment.

vendor/bin/phpunit --version

If the above command doesn't display the PHPUnit version, it means it's not installed. You can add it as a development dependency using Composer:

composer require --dev phpunit/phpunit

After installation, verify again:

vendor/bin/phpunit --version

2. Configure the phpunit.xml File

Copy the PHPUnit configuration file provided by Drupal to your project's root:

cp web/core/phpunit.xml.dist phpunit.xml

This file is essential for customizing test configurations according to your project's specific needs.

Contents of phpunit.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!-- For how to customize PHPUnit configuration, see core/tests/README.md. -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         bootstrap="docroot/core/tests/bootstrap.php" colors="true"
         beStrictAboutTestsThatDoNotTestAnything="true"
         beStrictAboutOutputDuringTests="true"
         beStrictAboutChangesToGlobalState="true"
         failOnWarning="true"
         printerClass="\Drupal\Tests\Listeners\HtmlOutputPrinter"
         cacheResult="false"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
  <php>
    <!-- PHP Settings -->
    <ini name="error_reporting" value="32767"/>
    <ini name="memory_limit" value="-1"/>
    <!-- Base URL for the site under test -->
    <env name="SIMPLETEST_BASE_URL" value="https://[your_project].ddev.site"/>
    <!-- Database connection string -->
    <env name="SIMPLETEST_DB" value="mysql://db:db@db/db"/>
    <!-- Output directory for browser tests -->
    <env name="BROWSERTEST_OUTPUT_DIRECTORY" value="./docroot/sites/simpletest/browser_output"/>
    <!-- Optional additional configurations -->
    <!-- <env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled"/> -->
    <!-- <env name="MINK_DRIVER_CLASS" value=""/> -->
    <!-- <env name="MINK_DRIVER_ARGS" value=""/> -->
    <!-- <env name="MINK_DRIVER_ARGS_WEBDRIVER" value=""/> -->
  </php>
  <testsuites>
    <testsuite name="unit">
      <!-- Paths to unit tests for custom modules -->
      <directory>./docroot/modules/custom/*/tests/src/Unit</directory>
      <directory>./docroot/modules/custom/*/modules/*/tests/src/Unit</directory>
    </testsuite>
    <testsuite name="kernel">
      <!-- Paths to Kernel tests for custom modules -->
      <directory>./docroot/modules/custom/*/tests/src/Kernel</directory>
      <directory>./docroot/modules/custom/*/modules/*/tests/src/Kernel</directory>
    </testsuite>
    <testsuite name="functional">
      <!-- Paths to functional tests for custom modules -->
      <directory>./docroot/modules/custom/*/tests/src/Functional</directory>
      <directory>./docroot/modules/custom/*/modules/*/tests/src/Functional</directory>
    </testsuite>
    <testsuite name="functional-javascript">
      <!-- Paths to functional JavaScript tests for custom modules -->
      <directory>./docroot/modules/custom/*/tests/src/FunctionalJavascript</directory>
      <directory>./docroot/modules/custom/*/modules/*/tests/src/FunctionalJavascript</directory>
    </testsuite>
    <!-- You can uncomment the following test suite if needed -->
    <!-- <testsuite name="build">
         <file>./tests/TestSuites/BuildTestSuite.php</file>
    </testsuite> -->
  </testsuites>
  <listeners>
    <!-- Drupal listener for additional testing hooks -->
    <listener class="\Drupal\Tests\Listeners\DrupalListener"></listener>
  </listeners>
  <!-- Settings for coverage reports -->
  <coverage>
    <include>
      <!-- Directories to include -->
      <directory>./includes</directory>
      <directory>./lib</directory>
      <directory>./modules</directory>
      <directory>../modules</directory>
      <directory>../sites</directory>
    </include>
    <exclude>
      <!-- Directories to exclude -->
      <directory>./modules/*/src/Tests</directory>
      <directory>./modules/*/tests</directory>
      <directory>../modules/*/src/Tests</directory>
      <directory>../modules/*/tests</directory>
      <directory>../modules/*/*/src/Tests</directory>
      <directory>../modules/*/*/tests</directory>
      <!-- Exclude .api.php files from coverage -->
      <directory suffix=".api.php">./lib/**</directory>
      <directory suffix=".api.php">./modules/**</directory>
      <directory suffix=".api.php">../modules/**</directory>
    </exclude>
  </coverage>
</phpunit>

Important Notes:

  • SIMPLETEST_BASE_URL: Replace [your_project] with your actual project's name in DDEV.
  • SIMPLETEST_DB: Ensure the connection string reflects the correct credentials and host in your DDEV environment. (Can you use ddev status for see data about connection db)*
  • Directory: You may need to change the directory to a valid one, in this case use docroot, but you could use web or something else.

Understanding Kernel Tests in Drupal

What Are Kernel Tests in Drupal?

Kernel tests are a mid-level automated testing approach that validates the integration between different system components within a reduced Drupal environment. These tests run using a lightweight Drupal bootstrap, allowing you to test interactions with core subsystems like the database and configuration system without loading the full presentation layer or a browser.

They are based on the KernelTestBase class, which provides access to a minimal Drupal environment, enabling the testing of integration scenarios that go beyond unit tests but without the complexity of full functional tests.

Why Are Kernel Tests Important?

  • Integration Validation: They ensure that custom components (entities, services, plugins, etc.) work correctly with Drupal's APIs and subsystems.
  • Efficiency: Kernel tests are faster than full functional tests since they don't require a web server or browser.
  • Intermediate Coverage: They provide deeper coverage than unit tests but are simpler to set up and run compared to functional or functional JavaScript tests.
  • Early Detection of Integration Issues: They help identify integration bugs between custom code and Drupal subsystems before they reach production.
  • Controlled Environment: They offer a consistent and controlled setup for testing subsystems like entity storage, custom queries, or event subscribers.

Common Use Cases for Kernel Tests in Drupal

1. CRUD Operations on Custom Entities

What to Test:

  • Create, load, update, and delete entities.
  • Ensure custom fields are saved and retrieved correctly.
Example:
public function testEntityCrudOperations() {
    $entity = \Drupal::entityTypeManager()
      ->getStorage('custom_entity')
      ->create(['field_custom' => 'value']);
    $entity->save();
    $loaded_entity = \Drupal::entityTypeManager()
      ->getStorage('custom_entity')
      ->load($entity->id());
    $this->assertEquals('value', $loaded_entity->get('field_custom')->value);
}

2. Custom Form Validation

What to Test:

  • Validate the behavior of custom forms built with the Form API.
  • Test restrictions on required fields or data formats.
Example:
use Drupal\Core\Form\FormState;
public function testCustomFormValidation() {
    $form = \Drupal::formBuilder()->getForm('Drupal\custom_module\Form\CustomForm');
    $form_state = new FormState();
    $form_state->setValues(['field_name' => 'Invalid Value']);
    
    // Execute validations$form['#validate'][0]($form, $form_state);
    $this->assertTrue($form_state->hasAnyErrors(), 'Validation failed as expected');
}

3. Custom Services

What to Test:

  • Ensure the service returns the correct values.
  • Validate that the service handles configurations or dependencies properly.

Example:

public function testCustomService() {
    $service = \Drupal::service('custom_module.some_service');
    $result = $service->processData(['input' => 'test']);
    $this->assertEquals('expected_output', $result);
}

4. Event Subscribers

What to Test:

  • Ensure the subscriber reacts correctly to specific events, such as hook_entity_insert.

Example:

public function testEntityInsertSubscriber() {
    $entity = \Drupal::entityTypeManager()
      ->getStorage('node')
      ->create(['type' => 'article', 'title' => 'Test']);
    $entity->save();
    // Verify that the subscriber modified the entity as expected.
    $this->assertEquals('Modified Title', $entity->getTitle());
}

5. Custom Queries

What to Test:

  • Validate that custom queries return correct results based on filters and conditions.

Example:

public function testCustomQuery() {
    $query = \Drupal::entityQuery('custom_entity')
      ->condition('field_status', 'active');
    $result = $query->execute();
    $this->assertNotEmpty($result, 'Query returned active entities');
}

6. Cache API

What to Test:

  • Verify that data is properly stored and retrieved from the cache.
  • Ensure data is invalidated under the correct conditions.

Example:

use Drupal\Core\Cache\CacheBackendInterface;

public function testCacheStorage() {
    $cache = \Drupal::cache('custom_cache');
    $cache->set('cache_key', 'cache_value', CacheBackendInterface::CACHE_PERMANENT);

    $cached_data = $cache->get('cache_key');
    $this->assertEquals('cache_value', $cached_data->data);
}

7. Typed Data API

What to Test:

  • Test validation and serialization of complex structured data.

Example:

public function testTypedDataValidation() {
    $data = \Drupal::typedDataManager()->create('string');
    $data->setValue('Test String');
    $violations = $data->validate();
    $this->assertTrue($violations->count() === 0, 'Validation passed for valid data');
}

8. Custom Rules

What to Test:

  • Ensure that conditions and actions of a custom rule work as expected.

Example:

public function testCustomRule() {
    $rule = \Drupal::service('rules')->load('custom_rule');
    $result = $rule->evaluate(['entity' => $entity]);
    $this->assertTrue($result, 'Rule evaluated correctly');
}

9. Migration test (No testing yet)

What to Test:

  • The idea is to test a migration and make it work.

Example:

  /**
   * Tests the migration process.
   */
  public function testMigration() {
    // Load the migration.
    $migrationPluginManager = \Drupal::service('plugin.manager.migration');
    $migration = $migrationPluginManager->createInstance('custom_migration_id');

    // Execute the migration.
    $executable = new MigrateExecutable($migration, new MigrateMessage());
    $result = $executable->import();

    // Verify results.
    $this->assertEquals(MigrateExecutable::RESULT_COMPLETED, $result, 'Migration completed successfully.');
    $this->assertCount(5, \Drupal::entityTypeManager()->getStorage('node')->loadMultiple(), 'Correct number of nodes migrated.');
  }

How to perform a kernel test:

Step 1: Create a Simple Custom Module

Create a custom module named example_module.

File Structure:

modules/
└── custom/
    └── example_module/
        ├── example_module.info.yml
        └── example_module.services.yml

Content of example_module.info.yml:

name: Example Module
type: module
description: 'A simple example module for testing purposes.'
package: Custom
core_version_requirement: ^10

Step 2: Create the Kernel Test

Create the directory for tests and the test file:

modules/
└── custom/
    └── example_module/
        ├── tests/
        │   └── src/
        │       └── Kernel/
        │           └── ExampleModuleKernelTest.php
        ├── example_module.info.yml
        └── example_module.services.yml

Content of ExampleModuleKernelTest.php:

<?php

namespace Drupal\Tests\example_module\Kernel;

use Drupal\KernelTests\KernelTestBase;

/**
 * Tests for the Example Module.
 *
 * @group example_module
 */
class ExampleModuleKernelTest extends KernelTestBase {

  /**
   * Modules to enable.
   *
   * @var array
   */
  protected static $modules = [
    'system',
    'user',
    'example_module',
  ];

  /**
   * Test that the module is enabled.
   */
  public function testModuleIsEnabled() {
    $this->assertTrue(\Drupal::moduleHandler()->moduleExists('example_module'), 'Example Module is enabled.');
  }

  /**
   * Test creating a user entity.
   */
  public function testCreateUser() {
    // Create a user entity.
    $user = $this->container->get('entity_type.manager')
      ->getStorage('user')
      ->create([
        'name' => 'test_user',
        'mail' => 'test_user@example.com',
        'status' => 1,
      ]);
    $user->save();

    // Load the user entity.
    $loaded_user = $this->container->get('entity_type.manager')
      ->getStorage('user')
      ->load($user->id());

    // Assert that the user was created successfully.
    $this->assertEquals('test_user', $loaded_user->getAccountName(), 'User account name matches.');
    $this->assertEquals('test_user@example.com', $loaded_user->getEmail(), 'User email matches.');
  }

}

Step 3: Run the Test

From your project's root directory, run:

ddev ssh and run that next command:
vendor/bin/phpunit modules/custom/example_module/tests/src/Kernel/ExampleModuleKernelTest.php --verbose

Code Explanation

  • Namespace: Drupal\Tests\example_module\Kernel defines the namespace for your module's tests.
  • Class ExampleModuleKernelTest: Extends KernelTestBase, providing the necessary environment for Kernel tests.
  • Property protected static $modules: Lists the modules that need to be enabled during the test. It includes 'system', 'user', and your 'example_module'.
  • Method testModuleIsEnabled: Checks that your custom module is enabled.
  • Method testCreateUser: Creates a user, saves it, reloads it, and verifies that the data matches the expected values.

Important Notes

  • Service Container: We use $this->container to access Drupal services like entity_type.manager.
  • Assertions: Methods like assertTrue and assertEquals check specific conditions to determine if the test passes or fails.
  • Cleanup: In more complex tests, you might need to clean up created data. In this case, the database resets between tests, so it's unnecessary.

What Does This Test Do?

The test performs the following:

  1. Verifies that the module is enabled: Ensures that example_module is active in the test environment.
  2. Creates and Verifies a User:
    • Creates a new user with specific username and email.
    • Saves the user to the database.
    • Loads the user from the database.
    • Checks that the loaded username and email match the ones provided.

This simple example covers key aspects:

  • Interaction with Drupal's entity system.
  • Use of the service container.
  • Making assertions to validate expected behavior.

Conclusion

Kernel tests are a powerful tool to ensure your custom modules function correctly within the Drupal ecosystem. By integrating them into your development workflow, you can efficiently detect and resolve integration issues, enhancing the quality and stability of your project.