Install
$ agentstack add skill-trebormc-drupal-ai-agents-drupal-functional-test ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Drupal Functional Test
What It Is
Installs a complete Drupal site and simulates HTTP requests with Mink + BrowserKit. It is NOT a real browser. JavaScript does NOT execute.
Speed: 4-120 seconds per class (installs Drupal from scratch per class).
When to Use
- Module configuration forms (admin forms)
- Content creation/editing/deletion via UI
- Verify permissions: without permission -> 403, with permission -> 200
- Verify HTML output of pages
- Verify that blocks appear where they should
- Test redirects and routes
- Verify status/error messages
When NOT to Use
- If you can test the same logic with Kernel test -> use Kernel (10x faster)
- If you need JavaScript/AJAX -> FunctionalJavascript
- If it is pure PHP logic -> Unit test
Required Properties
protected $defaultTheme = 'stark'; // REQUIRED. Use 'stark' for speed.
protected static $modules = ['my_module']; // Required modules.
// protected $profile = 'testing'; // Default. Only use 'standard' if you need its config.
Base Template
NOTE: if the flow needs JavaScript/AJAX, switch to the drupal-functionaljs-test skill. assertSession()->statusCodeEquals() works HERE but does NOT work in FunctionalJavascript tests.
drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$this->adminUser = $this->drupalCreateUser(['administer MODULE']);
}
public function testSomething(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/MODULE/settings');
$this->assertSession()->statusCodeEquals(200);
}
}
Pattern: Configuration Form Test
adminUser = $this->drupalCreateUser(['administer my_module']);
}
public function testFormSavesValues(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/my-module/settings');
$assert = $this->assertSession();
$assert->statusCodeEquals(200);
$assert->fieldExists('max_items');
$this->submitForm([
'max_items' => '25',
'cache_enabled' => TRUE,
], 'Save configuration');
$assert->statusMessageContains('The configuration options have been saved', 'status');
$config = $this->config('my_module.settings');
$this->assertSame(25, $config->get('max_items'));
$this->assertTrue($config->get('cache_enabled'));
// Verify the form displays saved values
$this->drupalGet('admin/config/my-module/settings');
$assert->fieldValueEquals('max_items', '25');
$assert->checkboxChecked('edit-cache-enabled');
}
public function testFormValidation(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/my-module/settings');
$this->submitForm(['max_items' => '-5'], 'Save configuration');
$this->assertSession()->statusMessageExists('error');
}
public function testFormRequiresPermission(): void {
$user = $this->drupalCreateUser(['access content']);
$this->drupalLogin($user);
$this->drupalGet('admin/config/my-module/settings');
$this->assertSession()->statusCodeEquals(403);
}
}
Pattern: Node Creation and Editing Test
/**
* @group my_module
*/
class ArticleFormTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
protected static $modules = ['node', 'field', 'text', 'filter', 'my_module'];
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$this->author = $this->drupalCreateUser([
'access content',
'create article content',
'edit own article content',
'delete own article content',
]);
}
public function testCreateArticle(): void {
$this->drupalLogin($this->author);
$this->drupalGet('node/add/article');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([
'title[0][value]' => 'Test Article',
'body[0][value]' => 'Body content.',
], 'Save');
$assert = $this->assertSession();
$assert->statusMessageContains('has been created', 'status');
$assert->pageTextContains('Test Article');
$node = $this->drupalGetNodeByTitle('Test Article');
$this->assertNotNull($node);
$this->assertSame('article', $node->bundle());
}
public function testEditArticle(): void {
$this->drupalLogin($this->author);
$node = $this->drupalCreateNode([
'type' => 'article',
'title' => 'Original',
'uid' => $this->author->id(),
]);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals('title[0][value]', 'Original');
$this->submitForm(['title[0][value]' => 'Updated'], 'Save');
$this->assertSession()->statusMessageContains('has been updated', 'status');
}
public function testDeleteArticle(): void {
$this->drupalLogin($this->author);
$node = $this->drupalCreateNode([
'type' => 'article',
'title' => 'To Delete',
'uid' => $this->author->id(),
]);
$this->drupalGet('node/' . $node->id() . '/delete');
$this->submitForm([], 'Delete');
$this->assertSession()->statusMessageContains('has been deleted', 'status');
}
public function testOtherUserCannotEdit(): void {
$other = $this->drupalCreateUser(['access content', 'edit own article content']);
$node = $this->drupalCreateNode([
'type' => 'article',
'uid' => $this->author->id(),
]);
$this->drupalLogin($other);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->statusCodeEquals(403);
}
}
Pattern: Page Output and Blocks Test
public function testBlockAppears(): void {
$this->drupalLogin($this->adminUser);
$this->drupalPlaceBlock('my_module_stats_block', [
'region' => 'content',
'label' => 'Stats',
'label_display' => TRUE,
]);
$this->drupalGet('');
$assert = $this->assertSession();
$assert->pageTextContains('Stats');
$assert->elementExists('css', '.block-my-module-stats-block');
}
Assertion Reference -- $this->assertSession()
$assert = $this->assertSession();
// HTTP status
$assert->statusCodeEquals(200);
// Visible text (no HTML)
$assert->pageTextContains('Welcome');
$assert->pageTextNotContains('Access denied');
// Raw HTML
$assert->responseContains('');
// Form fields
$assert->fieldExists('title[0][value]');
$assert->fieldValueEquals('title[0][value]', 'Expected');
$assert->checkboxChecked('edit-status-value');
// CSS elements
$assert->elementExists('css', '.my-class');
$assert->elementNotExists('css', '.should-not-exist');
$assert->elementTextContains('css', 'h1', 'Title');
$assert->elementsCount('css', '.item', 5);
// Links and buttons
$assert->linkExists('Log out');
$assert->linkByHrefExists('/node/add');
$assert->buttonExists('Save');
// Status messages (Drupal 9.3+)
$assert->statusMessageContains('has been created', 'status');
$assert->statusMessageExists('error');
$assert->statusMessageNotExists('error');
// URL
$assert->addressEquals('node/1');
Navigation Methods
$this->drupalGet('admin/config');
$this->drupalLogin($account);
$this->drupalLogout();
$this->submitForm(['field' => 'value'], 'Save');
$this->clickLink('Edit');
$node = $this->drupalCreateNode(['type' => 'article', 'title' => 'Test']);
$node = $this->drupalGetNodeByTitle('Test');
Anti-Patterns
- Do not use
drupalPostForm()-- it is deprecated. UsesubmitForm(). - Do not verify business logic with Functional tests if a Kernel test works.
- Do not forget
$defaultTheme. Without it the test fails. - Do not depend on theme markup. Search by text or classes you control.
- Do not use
assertSession()->statusCodeEquals()if you will later migrate to FunctionalJS (it does not support it). - Data provider methods must be
public static function(Drupal 10+11 compatibility).
Execution Command
First pick the config form ONCE per session: ssh web test -f phpunit.xml && echo "ROOT" || echo "CORE"
# Form ROOT (project phpunit.xml exists):
ssh web ./vendor/bin/phpunit $DDEV_DOCROOT/modules/custom/MODULE/tests/src/Functional
ssh web ./vendor/bin/phpunit --filter testFormSavesValues $DDEV_DOCROOT/modules/custom/MODULE/tests/src/Functional
# 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/Functional
Troubleshooting
| Error | Cause | Fix | |-------|-------|-----| | Unexpected 403 in test | Test user lacks permission | Pass the exact permissions array to drupalCreateUser([...]) | | "Theme not found" | Missing $defaultTheme | Add protected $defaultTheme = 'stark'; | | "Test site directory exists already" | Stale test site from aborted run | ssh web rm -rf $DDEV_DOCROOT/sites/simpletest/ then retry | | "Failed to connect" / base URL errors | SIMPLETESTBASEURL not set | Use Form CORE with explicit env vars (above) | | Class not found | Stale autoloader | ssh web composer dump-autoload |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: trebormc
- Source: trebormc/drupal-ai-agents
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.