Testing
Baander uses PHPUnit 12 with three test suites. Tests run inside the Docker container.
Test Suites
| Suite | Directory | Scope |
|---|---|---|
| Unit | tests/Unit/ |
Pure domain logic, no container, no framework |
| Functional | tests/Functional/ |
With Symfony kernel container, database, and services |
| Integration | tests/Integration/ |
External service integration tests |
Running Tests
All commands run inside the app container via make exec:
# Run all tests
make phpunit
# Run a single test file
make exec cmd="./vendor/bin/phpunit tests/Unit/Catalog/Domain/Model/AlbumTest.php"
# Run a specific suite
make exec cmd="./vendor/bin/phpunit --testsuite Unit"
# Run a single test method
make exec cmd="./vendor/bin/phpunit --filter testCreateAlbum"
# Run with Xdebug off (faster)
make exec cmd="XDEBUG_MODE=off ./vendor/bin/phpunit"
Coverage
make phpunit runs with HTML, Clover, and JUnit report generation:
- HTML:
reports/coverage/ - Clover:
reports/clover.xml - JUnit:
reports/junit.xml
Conventions
- Manual object construction — tests build domain objects directly (e.g.,
Album::create(...)) rather than using factories. Zenstruck Foundry is available but not the default convention. - Test file structure mirrors
src/— a test forsrc/Catalog/Domain/Model/Album.phplives intests/Unit/Catalog/Domain/Model/AlbumTest.php. - No mocks in domain tests — unit tests exercise real domain logic. Mocks are reserved for infrastructure and external dependencies.
Example: Unit Test (Domain Logic)
Unit tests live in tests/Unit/ and test pure domain logic with no framework or container:
final class AlbumTest extends TestCase
{
public function testCreateAlbum(): void
{
$album = Album::create(
libraryId: Uuid::generate(),
title: 'Abbey Road',
type: 'album',
);
$this->assertNotNull($album->getId());
$this->assertNotNull($album->getPublicId());
$this->assertSame('Abbey Road', $album->getTitle());
}
public function testCreateAlbumWithEmptyTitleThrows(): void
{
$this->expectException(InvalidArgumentException::class);
Album::create(
libraryId: Uuid::generate(),
title: '',
type: 'album',
);
}
}
Example: Unit Test (Application Handler)
Application handler tests mock domain interfaces (repositories, ports) but test real orchestration logic:
final class StartPlaybackHandlerTest extends TestCase
{
public function testStartsPlaybackAndDispatchesEvent(): void
{
$sessionPort = $this->createMock(PartySessionPortInterface::class);
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$handler = new StartPlaybackHandler($sessionPort, $eventDispatcher);
$session = /* ... create or reconstitute a PartySession ... */;
$sessionPort->method('findByUuid')->willReturn($session);
$sessionPort->expects($this->once())->method('startPlayback');
$eventDispatcher->expects($this->once())->method('dispatch');
$command = new StartPlaybackCommand(sessionId: $session->getId(), position: 0);
($handler)($command);
}
}
Example: Functional Test (API Endpoint)
Functional tests use the Symfony kernel container and hit real endpoints:
final class PlaylistControllerTest extends WebTestCase
{
public function testCreatePlaylist(): void
{
$client = static::createClient();
$client->loginUser($this->createTestUser());
$client->request('POST', '/api/playlists', [
'json' => ['name' => 'Chill Vibes', 'isPublic' => false],
]);
$this->assertResponseIsSuccessful();
$this->assertJsonContains(['data' => ['name' => 'Chill Vibes']]);
}
}
Static Analysis
PHPStan runs alongside tests:
# Run PHPStan
make phpstan
# Generate a baseline for existing errors
make phpstan-baseline
Frontend Testing
The web frontend uses Vitest:
cd ui/web
yarn test # Run tests
yarn test:watch # Watch mode
yarn test:coverage # With coverage
See the Frontend Development page for more details.