Progressive Testing Strategy
When to Use
Use this progressive approach when building testing coverage for a new module or improving coverage for an existing module. Start simple and add complexity as the module matures.
Decision: Testing Coverage Phases
| Phase | Coverage | Test Types | Focus |
|---|---|---|---|
| MVP | 30-40% | Unit + Kernel | Core logic and integration |
| Feature Complete | 60-70% | + Functional | Admin UI and workflows |
| Production Ready | 80%+ | + FunctionalJS + Gander | JavaScript and performance |
Pattern: Phase 1 - Foundation Testing (MVP)
// Create abstract base classes for consistency
// tests/src/Unit/MyModuleUnitTestBase.php
namespace Drupal\Tests\my_module\Unit;
use Drupal\Tests\UnitTestCase;
abstract class MyModuleUnitTestBase extends UnitTestCase {
// Common mocks and setup for unit tests
protected function getConfigStub(array $config = []): object {
return $this->getConfigFactoryStub([
'my_module.settings' => $config,
]);
}
}
// tests/src/Kernel/MyModuleKernelTestBase.php
namespace Drupal\Tests\my_module\Kernel;
use Drupal\KernelTests\KernelTestBase;
abstract class MyModuleKernelTestBase extends KernelTestBase {
protected static $modules = ['system', 'user', 'my_module'];
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installConfig(['my_module']);
}
}
// tests/src/Kernel/EntityProcessorTest.php
// The base class stays bare; the class PHPUnit runs carries the attribute.
namespace Drupal\Tests\my_module\Kernel;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
#[Group('my_module')]
#[RunTestsInSeparateProcesses]
class EntityProcessorTest extends MyModuleKernelTestBase {
public function testProcessing(): void {
$processor = $this->container->get('my_module.entity_processor');
$this->assertNotNull($processor);
}
}
Priority: Test core business logic and Drupal integration first.
Pattern: Phase 2 - Feature Testing
// tests/src/Functional/MyModuleFunctionalTestBase.php
namespace Drupal\Tests\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
abstract class MyModuleFunctionalTestBase extends BrowserTestBase {
protected $defaultTheme = 'stark';
protected static $modules = ['my_module'];
protected function setUp(): void {
parent::setUp();
// Create standard test users
$this->adminUser = $this->drupalCreateUser([
'administer my module',
'access administration pages',
]);
}
}
Priority: Add admin interface and user workflow testing.
Pattern: Phase 3 - Production Ready
// tests/src/FunctionalJavascript/MyModuleJavaScriptTestBase.php
namespace Drupal\Tests\my_module\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
abstract class MyModuleJavaScriptTestBase extends WebDriverTestBase {
protected $defaultTheme = 'stark';
protected static $modules = ['my_module'];
}
// tests/src/FunctionalJavascript/MyModulePerformanceTestBase.php
namespace Drupal\Tests\my_module\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\PerformanceTestBase;
abstract class MyModulePerformanceTestBase extends PerformanceTestBase {
protected static $modules = ['my_module'];
protected $profile = 'minimal';
}
Priority: Add JavaScript interactions and performance validation.
Common Mistakes
- Trying to achieve 100% coverage immediately → Burnout and abandoned testing
- Writing performance tests before features stabilize → Wasted effort on changing code
- Skipping base test classes → Code duplication across test files
- Not prioritizing high-risk code → Testing trivial code while critical paths untested
- Writing all tests at once → No early feedback, integration issues found late
WHY these are mistakes: Testing is an investment. Start with high-value tests that catch the most bugs. Building complete coverage takes time - progressive approach maintains momentum. Base classes reduce duplication and make tests easier to maintain. Performance testing unstable features wastes time because budgets change constantly. Early testing provides early feedback - don't wait until all features are complete.