testing.md
April 14, 2026 ยท View on GitHub
dde uses PHPUnit for all tests. Tests are organized into unit tests and E2E tests, with strict separation.
Test Structure
Tests mirror the src/ directory structure:
tests/
Unit/
Manager/
ImageManagerTest.php
ConfigManagerTest.php
...
Doctor/
Check/
BinaryPathCheckTest.php
...
...
E2E/
...
Running Tests
# All tests except E2E (default for CI)
make test
# Unit tests only
make test-unit
# E2E tests (requires Docker)
make test-e2e
# Tests with coverage report
make test-coverage
Test Categories
Unit Tests
Unit tests verify individual classes in isolation. They mock dependencies and do not require Docker or any external services.
Every new class should have a corresponding unit test covering at minimum:
- The happy path (expected behavior with valid inputs)
- The most important error cases (invalid inputs, edge cases)
E2E Tests
E2E tests require a running Docker daemon and are tagged with:
#[Group('e2e')]
These tests are excluded from the standard test suite and CI. Run them explicitly with make test-e2e.
Full QA Suite
The make qa target runs the complete quality assurance pipeline:
- ECS -- coding standard check and auto-fix
- PHPStan -- static analysis at level 8
- Rector -- automated refactoring (dry-run in QA mode)
- Tests -- PHPUnit excluding E2E
All four steps must pass before a commit is considered ready.
Writing Tests
Example: Unit Test
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Manager;
use App\Manager\ImageManager;
use App\Manager\DockerManager;
use App\Model\UserContext;
use PHPUnit\Framework\TestCase;
final class ImageManagerTest extends TestCase
{
public function testHasLabelReturnsFalseForMissingImage(): void
{
$dockerManager = $this->createMock(DockerManager::class);
$dockerManager->method('inspect')
->willThrowException(new \RuntimeException('not found'));
$userContext = new UserContext(uid: 1000, gid: 1000);
$manager = new ImageManager($dockerManager, $userContext);
self::assertFalse($manager->hasLabel('nonexistent:latest', 'dde.configured'));
}
}
Conventions
- Test classes are
final - Test methods use
testprefix (not@testannotation) - Use
self::assert*()instead of$this->assert*() - Mock external dependencies (Docker, filesystem, processes)
- Each test method tests one behavior