Skip to content

Best Practices & Anti-Patterns

When to Use

Consult this guide when writing tests, reviewing code, or establishing testing standards for your team.

Decision: Security Best Practices

Practice Implementation Why
Test access control Create users with different permissions, verify access denied Prevents unauthorized access bugs
Test CSRF protection Verify form tokens required Prevents cross-site request forgery
Test input validation Submit malicious input, verify sanitization Prevents XSS and injection attacks
Test permission boundaries Test with minimal permissions, verify failures Prevents privilege escalation

Pattern: Security Testing

public function testAccessControl(): void {
  // Test admin access
  $admin = $this->drupalCreateUser(['administer my module']);
  $this->drupalLogin($admin);
  $this->drupalGet('admin/config/my-module');
  $this->assertSession()->statusCodeEquals(200);

  // Test unauthorized access
  $user = $this->drupalCreateUser(['access content']);
  $this->drupalLogin($user);
  $this->drupalGet('admin/config/my-module');
  $this->assertSession()->statusCodeEquals(403);

  // Test anonymous access
  $this->drupalLogout();
  $this->drupalGet('admin/config/my-module');
  $this->assertSession()->statusCodeEquals(403);
}

public function testXssProtection(): void {
  $malicious = '<script>alert("XSS")</script>';

  $this->submitForm(['title' => $malicious], 'Save');

  // Verify sanitization
  $this->assertSession()->responseNotContains('<script>');
  $this->assertSession()->responseContains(htmlspecialchars($malicious));
}

Decision: Performance Best Practices

Practice Implementation Why
Test query counts Assert database query limits Detects N+1 query problems
Test cache efficiency Compare cold vs hot cache metrics Verifies caching works
Test cache invalidation Verify caches cleared on updates Prevents stale data
Test bulk operations Process large datasets, measure performance Catches scalability issues

Pattern: Performance Testing

public function testQueryEfficiency(): void {
  // Track queries
  \Drupal::service('database')->enableQueryLog();

  // Perform operation
  $service = \Drupal::service('my_module.processor');
  $service->processList($items);

  $queries = \Drupal::service('database')->getLog();

  // Assert reasonable query count (not N+1)
  $this->assertLessThanOrEqual(10, count($queries));
}

public function testCacheWarmth(): void {
  // Cold cache - expect queries
  $this->drupalGet('my-module/cached-page');
  $cold_performance = $this->getPerformanceData();
  $this->assertGreaterThan(10, $cold_performance['queries']);

  // Hot cache - expect no queries
  $this->drupalGet('my-module/cached-page');
  $hot_performance = $this->getPerformanceData();
  $this->assertLessThan(3, $hot_performance['queries']);
}

Decision: Development Standards

Practice Implementation Why
Use data providers @dataProvider for multiple test cases Reduces code duplication
Test one behavior per method Each test verifies one thing Easier debugging
Use descriptive test names Name describes what's tested Self-documenting tests
Document complex tests PHPDoc explains test scenario Maintainability
Mock external dependencies Never call real APIs in tests Fast, reliable tests

Pattern: Development Standards

/**
 * Tests email validation with various input formats.
 *
 * @dataProvider emailProvider
 */
public function testEmailValidation($email, $expected_valid, $description): void {
  $result = $this->validator->validateEmail($email);
  $this->assertEquals($expected_valid, $result, $description);
}

public function emailProvider(): array {
  return [
    'valid email' => ['test@example.com', TRUE, 'Standard email format'],
    'invalid format' => ['notanemail', FALSE, 'Missing @ symbol'],
    'empty string' => ['', FALSE, 'Empty input'],
    'no domain' => ['test@', FALSE, 'Missing domain'],
    'international domain' => ['test@münchen.de', TRUE, 'IDN domain'],
  ];
}

Common Anti-Patterns

Anti-Pattern 1: Using database queries in Unit tests

  • Wrong: Accessing database in Unit tests → Slow tests, database dependencies
  • Right: Mock data instead → Fast, isolated tests

Anti-Pattern 2: Not waiting for AJAX in JavaScript tests

  • Wrong: Checking content immediately after AJAX trigger → Race conditions, flaky tests
  • Right: Call assertWaitOnAjaxRequest() → Reliable test execution

Anti-Pattern 3: Using sleep() instead of proper waits

  • Wrong: Fixed timing with sleep(2) → Unreliable, slow tests
  • Right: Wait for specific conditions with waitForText() → Reliable, fast as possible

Anti-Pattern 4: Testing multiple behaviors in one method

  • Wrong: One test method verifies multiple unrelated things → Hard to debug failures
  • Right: One behavior per test method → Clear failure messages

Anti-Pattern 5: Not testing permission boundaries

  • Wrong: Only testing happy path with admin user → Security vulnerabilities
  • Right: Test with users who should NOT have access → Catch access control bugs

See Also