Writing Integration Tests (KernelTests) in Drupal
Posted by
admin
Wednesday April
22nd
, 2026 4:42
p.m.
Drupal integration tests use the KernelTestBase class (from Drupal\KernelTests), which boots a minimal Drupal kernel with a real database but without a full HTTP stack. This sits between unit tests (no kernel, no DB) and functional tests (full browser simulation).
Setup
Your test class lives in modules/my_module/tests/src/Kernel/ and extends KernelTestBase:
<?php
namespace Drupal\my_module\Tests\Kernel;
use Drupal\KernelTests\KernelTestBase;
class MyServiceTest extends KernelTestBase {
protected static $modules = [
'system',
'user',
'my_module',
];
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installConfig(['my_module']);
$this->installSchema('system', ['sequences']);
}
}
Key things in setUp():
$modules— declare every module your test depends on; the kernel only loads what you listinstallEntitySchema('node')— creates DB tables for an entity typeinstallConfig(['my_module'])— imports your module's config YAMLsinstallSchema('module', ['table'])— installs a specific DB schema (for non-entity tables)
Writing Test Methods
public function testMyService(): void {
$service = $this->container->get('my_module.my_service');
$result = $service->doSomething();
$this->assertEquals('expected', $result);
}
The $this->container is a real DI container, so you get actual service instances, not mocks.
Creating Entities in Tests
Use the entity API directly — no need for form submission:
$node = \Drupal\node\Entity\Node::create([
'type' => 'article',
'title' => 'Test node',
'uid' => 1,
]);
$node->save();
$loaded = \Drupal\node\Entity\Node::load($node->id());
$this->assertEquals('Test node', $loaded->label());
For this to work, you'd need installEntitySchema('node') and the node module listed in $modules.
Testing Config
$config = $this->config('my_module.settings');
$config->set('my_key', 'my_value')->save();
$this->assertEquals('my_value', $this->config('my_module.settings')->get('my_key'));
Testing Event Subscribers / Hooks
For something like the CSP event subscriber work you did with seckit, you'd dispatch the event manually via the event dispatcher service:
$dispatcher = $this->container->get('event_dispatcher');
$event = new \Drupal\my_module\Event\MyEvent($someData);
$dispatcher->dispatch($event, MyEvent::EVENT_NAME);
$this->assertEquals('expected_result', $event->getSomething());
Testing Database Queries
Since there's a real DB, you can test queries directly:
$connection = $this->container->get('database');
$count = $connection->select('my_table', 't')
->countQuery()
->execute()
->fetchField();
$this->assertEquals(1, $count);
Running Tests
# Run a single test class
./vendor/bin/phpunit modules/my_module/tests/src/Kernel/MyServiceTest.php
# Run all kernel tests in a module
./vendor/bin/phpunit modules/my_module/tests/src/Kernel/
# With a specific phpunit config
./vendor/bin/phpunit -c core/phpunit.xml.dist modules/my_module/
Your phpunit.xml.dist (or a local phpunit.xml) needs SIMPLETEST_DB set:
<env name="SIMPLETEST_DB" value="mysql://drupal:drupal@localhost/drupal"/>
When to Use What
Test Type | Class | Use For |
|---|---|---|
Unit |
| Pure logic, no DB, no services |
Kernel |
| Services, entities, config, DB queries, event subscribers |
Functional |
| Form submission, routing, rendered HTML |
FunctionalJS |
| JavaScript behavior |
For most service/module work in mymodule, kernel tests are the sweet spot — fast enough to run frequently, real enough to catch integration bugs that unit tests miss.
The core point is that they let you test real Drupal behavior without the overhead of a full browser stack.
The Testing Spectrum Problem
Unit tests are fast but fake — you mock everything, so you're really just testing that your code calls the things you told it to call. Functional tests are real but slow — they boot a full Drupal install, simulate HTTP requests, render pages. For a lot of backend work, neither is ideal.
Kernel tests sit in the middle: a real database, a real service container, real entity and config systems — but no routing, no controllers, no page rendering.
What That Buys You
You catch wiring bugs. Unit tests can't tell you that your service is misconfigured in the service container, or that your schema doesn't match what the entity system expects, or that your config schema has a type mismatch. Kernel tests will catch all of that because it actually runs.
You test things that are inseparable from Drupal's internals. Entity CRUD, config system reads/writes, hook invocations, event subscribers, cache tag invalidation, database queries — these things don't make sense to mock. The whole point is that they work with Drupal, not in isolation.
You get a feedback loop fast enough to actually use. A functional test might take 30–60 seconds per test. A kernel test typically runs in under 5 seconds. That difference matters when you're iterating.
The Practical Case
Think about a CSP event subscriber written for seckit. A unit test there would be almost meaningless — you'd mock the event, mock the response, and just verify your method was called. A functional test would work but would be slow and test way more than the subscriber itself.
A kernel test lets you boot the real event dispatcher, fire the real event, and assert the real response headers were modified — which is exactly the contract you care about.
Same logic applies to anything in mymodule that touches entities, config, the database, or services interacting with each other. If two services collaborate through the container, a unit test of each in isolation can both pass while the integration is broken. A kernel test catches that.
The Short Version
Kernel tests exist because a lot of Drupal code's correctness is its integration with Drupal — and unit tests can't verify that, while functional tests are too expensive to run constantly. They're the right tool for backend module work where you care that the pieces actually fit together, not just that each piece does what you told it to do in isolation.