Anti-Patterns & Common Mistakes
When to Use
Avoid these patterns that lead to slow, brittle, unmaintainable tests.
Anti-Patterns
Using Wrong Test Type
Problem: BrowserTestBase for testing service logic Why it's bad: 10-30 seconds per test vs 0.5 seconds for Kernel test -- no benefit, massive cost Fix: Use lightest test type that can verify requirement -- Unit for pure logic, Kernel for services/database, Browser only for HTTP simulation
Example:
// ANTI-PATTERN: Browser test for service logic
class MyServiceTest extends BrowserTestBase {
public function testServiceMethod(): void {
$this->drupalGet('/'); // Unnecessary HTTP overhead
$service = \Drupal::service('my_module.service');
$result = $service->calculate(10);
$this->assertEquals(20, $result);
}
}
// CORRECT: Kernel test
class MyServiceTest extends KernelTestBase {
public function testServiceMethod(): void {
$service = $this->container->get('my_module.service');
$result = $service->calculate(10);
$this->assertEquals(20, $result);
}
}
Testing Implementation Instead of Behavior
Problem: Asserting on internal method calls instead of outcomes Why it's bad: Tests break when refactoring, even if behavior unchanged Fix: Assert on observable behavior -- return values, database state, rendered output
Example:
// ANTI-PATTERN: Testing implementation
public function testServiceCallsLogger(): void {
$mock_logger = $this->createMock(LoggerInterface::class);
$mock_logger->expects($this->once())->method('info'); // Brittle
$service = new MyService($mock_logger);
$service->doSomething();
}
// CORRECT: Testing behavior
public function testServiceReturnsProcessedData(): void {
$service = $this->container->get('my_module.service');
$result = $service->doSomething();
$this->assertEquals('expected result', $result);
}
Over-Mocking
Problem: Mocking every dependency with complex mock setup Why it's bad: Test becomes harder to maintain than the code it tests; doesn't verify real integration Fix: Use Kernel test to leverage real container, or question if code is too coupled
Example:
// ANTI-PATTERN: Mocking 5+ dependencies
public function testComplexService(): void {
$mock1 = $this->createMock(Service1::class);
$mock2 = $this->createMock(Service2::class);
$mock3 = $this->createMock(Service3::class);
// ... 10 lines of mock setup ...
$service = new ComplexService($mock1, $mock2, $mock3);
// Test becomes unreadable
}
// CORRECT: Use Kernel test with real container
public function testComplexService(): void {
$service = $this->container->get('my_module.complex_service');
$result = $service->process(['data' => 'value']);
$this->assertEquals('expected', $result);
}
Sleep Instead of Wait
Problem: Using sleep(2) in JavaScript tests
Why it's bad: Flaky tests (sometimes 2 seconds isn't enough), unnecessarily slow
Fix: Use waitForElement(), waitForText(), or custom wait conditions
Example:
// ANTI-PATTERN: Hardcoded sleep
public function testAjax(): void {
$page->click('#trigger-ajax');
sleep(2); // Sometimes fails, sometimes wastes time
$this->assertSession()->pageTextContains('Result');
}
// CORRECT: Wait for condition
public function testAjax(): void {
$page->click('#trigger-ajax');
$this->assertSession()->waitForText('Result');
}
Testing Core Functionality
Problem: Writing tests that verify Drupal core behavior Why it's bad: Wastes time, core already tested, doesn't verify YOUR code Fix: Test your custom logic's integration with core, not core itself
Example:
// ANTI-PATTERN: Testing core
public function testNodeSaveWorks(): void {
$node = Node::create(['type' => 'page', 'title' => 'Test']);
$node->save();
$this->assertNotNull($node->id()); // Testing core
}
// CORRECT: Testing your custom logic
public function testCustomNodeProcessing(): void {
$node = Node::create(['type' => 'page', 'title' => 'Test']);
$service = $this->container->get('my_module.node_processor');
$result = $service->processNode($node);
$this->assertEquals('expected_transformation', $result);
}
Shared Test State
Problem: Tests depend on execution order or shared state
Why it's bad: Tests fail when run in isolation, hard to debug
Fix: Each test creates its own fixtures in setUp() or test method
Example:
// ANTI-PATTERN: Shared state
protected $sharedNode; // Dangerous
public function testA(): void {
$this->sharedNode = $this->createNode();
// ...
}
public function testB(): void {
// Assumes testA ran first -- fragile
$this->assertEquals('page', $this->sharedNode->bundle());
}
// CORRECT: Independent tests
public function testA(): void {
$node = $this->createNode();
// Test using $node
}
public function testB(): void {
$node = $this->createNode();
// Test using $node
}
Common Mistakes
Security Testing
- Not testing permission denials -- users access restricted content
- Using admin user for all tests -- permission checks never verified
- Not testing CSRF tokens -- forms vulnerable to cross-site requests
- Not testing input sanitization -- XSS vulnerabilities
Performance Testing
- Installing unnecessary modules -- 2x-5x slower tests
- Using Browser tests when Kernel would work -- 10x slower
- Not using
@grouptags -- can't run fast tests separately - Running full test suite on every commit -- slow feedback loop
Reliability
- Hardcoding IDs or paths -- breaks when data changes
- Brittle CSS selectors (
.container > div:nth-child(3)) -- breaks when HTML changes - Not cleaning up test data in Kernel tests -- database pollution
- Expecting specific error messages -- breaks when wording changes (test error code/type instead)
Development Workflow
- Writing tests after implementation -- misses TDD benefits
- Not running tests before commit -- broken code reaches repo
- Ignoring deprecation warnings -- tests break on Drupal upgrade
- Not using coverage reports -- dead code accumulates