# Drupal Kernel Test

> >-

- **Type:** Skill
- **Install:** `agentstack add skill-trebormc-drupal-ai-agents-drupal-kernel-test`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [trebormc](https://agentstack.voostack.com/s/trebormc)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [trebormc](https://github.com/trebormc)
- **Source:** https://github.com/trebormc/drupal-ai-agents/tree/main/.claude/skills/drupal-kernel-test

## Install

```sh
agentstack add skill-trebormc-drupal-ai-agents-drupal-kernel-test
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Drupal Kernel Test

## What It Is

A Kernel test partially boots the Drupal kernel: service container,
database (SQLite by default), entity system, configuration and hooks.
There is NO web server or browser.

Speed: 1-10 seconds per test (vs 10-120s for Functional).

This is the default test for custom modules. If in doubt between Kernel and Functional,
choose Kernel. Only upgrade to Functional when you need to verify rendered HTML output.

## When to Use

- Custom services that use entity storage, database, config, or the container
- Entity CRUD (create, read, update, delete)
- Database queries (entity queries, select queries)
- Access logic and permissions (without verifying UI)
- Plugin managers with real discovery
- Event subscribers
- Hook implementations
- Token replacement
- Migrations (migrate API)
- Field types, formatters, widgets (logic, not rendering)
- Form submit/validate handlers (handler logic, not the rendered form)
- Queue workers
- Cron hooks

## When NOT to Use

- Need to verify rendered HTML -> Functional
- Need to submit forms via UI -> Functional
- Need JavaScript -> FunctionalJavascript
- Pure PHP logic without Drupal dependencies -> Unit

## Base Template

```php
installEntitySchema('user');
    $this->installConfig(['system', 'MODULE']);
  }

  public function testSomething(): void {
    $service = $this->container->get('MODULE.my_service');
    // assertions...
  }

}
```

## Setup Methods -- What to Use for What

### installEntitySchema('entity_type_id')

Installs DB tables for a content entity. Required before doing CRUD
with that entity. DO NOT use for config entities (they have no tables).

```php
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installEntitySchema('taxonomy_term');
```

### installConfig(['module_name'])

Imports default config (`config/install/` and `config/optional/`) from modules.
Needed when your code reads configuration.

```php
$this->installConfig(['system', 'node', 'filter', 'my_module']);
```

### installSchema('module', ['table'])

Installs tables defined in `hook_schema()`. Only for custom tables, not for entities.

```php
$this->installSchema('my_module', ['my_module_tracking']);
```

### Container Access

```php
$entityTypeManager = $this->container->get('entity_type.manager');
$configFactory = $this->container->get('config.factory');
$myService = $this->container->get('my_module.calculator');
```

## Pattern: Custom Service Test

```php
installEntitySchema('user');
    $this->installEntitySchema('node');
    $this->installConfig(['system', 'node', 'filter', 'my_module']);

    NodeType::create(['type' => 'article', 'name' => 'Article'])->save();
    $this->analyzer = $this->container->get('my_module.article_analyzer');
  }

  /**
   * @covers ::analyze
   */
  public function testAnalyzePublishedArticle(): void {
    $node = Node::create([
      'type' => 'article',
      'title' => 'Test Article',
      'status' => 1,
    ]);
    $node->save();

    $result = $this->analyzer->analyze($node);

    $this->assertSame('published', $result->getStatus());
    $this->assertGreaterThan(0, $result->getWordCount());
  }

  /**
   * @covers ::analyze
   */
  public function testAnalyzeThrowsOnInvalidBundle(): void {
    $this->expectException(\InvalidArgumentException::class);

    NodeType::create(['type' => 'page', 'name' => 'Page'])->save();
    $node = Node::create(['type' => 'page', 'title' => 'Not article']);
    $node->save();

    $this->analyzer->analyze($node);
  }

}
```

## Pattern: Entity CRUD Test

```php
public function testCreateAndLoadEntity(): void {
  $node = Node::create([
    'type' => 'article',
    'title' => 'My article',
    'status' => 0,
  ]);
  $node->save();

  // Reload from DB to verify real persistence
  $loaded = Node::load($node->id());
  $this->assertNotNull($loaded);
  $this->assertSame('My article', $loaded->getTitle());
  $this->assertFalse((bool) $loaded->isPublished());
}

public function testUpdateEntity(): void {
  $node = Node::create(['type' => 'article', 'title' => 'Original']);
  $node->save();

  $node->setTitle('Updated');
  $node->save();

  $loaded = Node::load($node->id());
  $this->assertSame('Updated', $loaded->getTitle());
}

public function testDeleteEntity(): void {
  $node = Node::create(['type' => 'article', 'title' => 'To delete']);
  $node->save();
  $nid = $node->id();

  $node->delete();
  $this->assertNull(Node::load($nid));
}
```

## Pattern: Access and Permissions Test

```php
use Drupal\user\Entity\User;
use Drupal\user\Entity\Role;

public function testAccessCheck(): void {
  $user_without_permission = User::create(['name' => 'no_access', 'status' => 1]);
  $user_without_permission->save();

  $role = Role::create([
    'id' => 'my_role',
    'label' => 'My Role',
  ]);
  $role->grantPermission('access my_module reports');
  $role->save();

  $user_with_permission = User::create(['name' => 'with_access', 'status' => 1]);
  $user_with_permission->addRole('my_role');
  $user_with_permission->save();

  $checker = $this->container->get('my_module.access_checker');
  $this->assertFalse($checker->canViewReports($user_without_permission));
  $this->assertTrue($checker->canViewReports($user_with_permission));
}
```

## Pattern: Configuration Test

```php
public function testDefaultConfig(): void {
  $config = $this->config('my_module.settings');
  $this->assertSame(10, $config->get('max_items'));
  $this->assertTrue($config->get('cache_enabled'));
}

public function testConfigChange(): void {
  $config = $this->config('my_module.settings');
  $config->set('max_items', 50)->save();

  $service = $this->container->get('my_module.listing');
  $this->assertSame(50, $service->getMaxItems());
}
```

## Pattern: Event Subscriber Test

```php
public function testEventSubscriber(): void {
  $node = Node::create(['type' => 'article', 'title' => 'Test', 'status' => 1]);
  $node->save();

  $event = new ArticlePublishedEvent($node);
  $dispatcher = $this->container->get('event_dispatcher');
  $dispatcher->dispatch($event, ArticlePublishedEvent::EVENT_NAME);

  $this->assertTrue($event->wasProcessed());
}
```

## Pattern: Auxiliary Test Module

When you need routes, services, or config that only exist for the test:

```yaml
# tests/modules/my_module_test/my_module_test.info.yml
name: 'My Module Test'
type: module
core_version_requirement: ^10 || ^11
package: Testing
dependencies:
  - my_module:my_module
hidden: true
```

In the test:
```php
protected static $modules = ['my_module', 'my_module_test'];
```

## Useful Core Traits

```php
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\Traits\Core\CronRunTrait;

class MyTest extends KernelTestBase {
  use NodeCreationTrait;
  use UserCreationTrait;
  use ContentTypeCreationTrait;
  use CronRunTrait;
}
```

## Anti-Patterns

1. Do not install unnecessary modules in `$modules`. Each one adds time.
2. Do not use `installEntitySchema()` for config entities. They have no tables.
3. Do not use `enableModules()` in setUp(). Use the `$modules` property.
4. Do not make assertions about rendered HTML. That is a Functional test.
5. Do not create unnecessary fixtures. Only what the test needs.
6. Do not forget to reload entities. After modifying, use `Entity::load($id)` to verify persisted state.

## Running Tests

First pick the config form ONCE per session: `ssh web test -f phpunit.xml && echo "ROOT" || echo "CORE"`

```bash
# Form ROOT (project phpunit.xml exists — it already sets SIMPLETEST_DB):
ssh web ./vendor/bin/phpunit $DDEV_DOCROOT/modules/custom/MODULE/tests/src/Kernel
ssh web ./vendor/bin/phpunit --filter testName $DDEV_DOCROOT/modules/custom/MODULE/tests/src/Kernel

# Form CORE (no project phpunit.xml — pass env vars explicitly):
ssh web env SIMPLETEST_DB=mysql://db:db@db/db SIMPLETEST_BASE_URL=http://localhost \
  ./vendor/bin/phpunit -c $DDEV_DOCROOT/core $DDEV_DOCROOT/modules/custom/MODULE/tests/src/Kernel
```

Never use `-c core` alone and never rely on `--testsuite` — see the **drupal-testing** rule for the full canonical pattern. Do not `export` env vars in the agent container (they would not reach the web container via SSH) — pass them inline with `env` as shown.

## Troubleshooting

| Error | Cause | Fix |
|-------|-------|-----|
| "Unknown entity type X" | Schema not installed or module missing | Add `$this->installEntitySchema('X')` in setUp() AND the providing module to `$modules` |
| "Base table or view not found" | Missing schema install | Add the matching `installSchema()` / `installEntitySchema()` call in setUp() |
| "Service not found" | Providing module not enabled | Add the module that defines the service to `$modules` |
| `Class not found` | Stale autoloader or wrong namespace | `ssh web composer dump-autoload`; verify namespace matches directory path |
| "Could not connect to database" | SIMPLETEST_DB not set | Use Form CORE with explicit env vars (above) |

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [trebormc](https://github.com/trebormc)
- **Source:** [trebormc/drupal-ai-agents](https://github.com/trebormc/drupal-ai-agents)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-trebormc-drupal-ai-agents-drupal-kernel-test
- Seller: https://agentstack.voostack.com/s/trebormc
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
