Streak provides a robust testing infrastructure that makes it easy to test your event-sourced applications. The testing approach is based on the Given-When-Then pattern, which is particularly well-suited for event-sourced systems.
The testing infrastructure provides several helpful components:
-
In-Memory Event Store
- Used for testing without a real database
- Maintains events in memory during tests
- Supports all Event Store operations
-
Null Event Bus
- No-op implementation for testing
- Useful when event publishing is not the focus of the test
-
Test Doubles
- Snapshotter implementations for testing
- Command bus implementations for testing
- Repository implementations for testing
Streak provides several scenario builders for different testing contexts:
-
Aggregate Root Testing (
AggregateRoot\TestCase)class MyAggregateTest extends AggregateRoot\TestCase { public function testCommand(): void { $this->for($id) ->given($event1, $event2) ->when($command) ->then($expectedEvent1, $expectedEvent2); } }
-
Event Listener Testing (
Listener\TestCase)class MyListenerTest extends Listener\TestCase { public function testEventHandling(): void { $this->given($event1, $event2) ->when($newEvent) ->then($expectedCommand) ->assert(); } }
When testing event-sourced aggregates:
- Use
given()to set up the aggregate's history - Use
when()to execute a command - Use
then()to verify the produced events
public function testDeactivateProject(): void
{
/** @var ProjectId $projectId */
$projectId = new ProjectId('project-123');
$this->for($projectId)
->given(
new ProjectCreated($projectId, 'Project Name'),
new ProjectStarted($projectId)
)
->when(new DeactivateProject($projectId))
->then(new ProjectDeactivated($projectId));
}When testing event listeners:
- Use
given()to set up the listener's state - Use
when()to send a new event - Use
then()to verify commands or state changes
public function testProjectionUpdated(): void
{
/** @var ProjectId $projectId */
$projectId = new ProjectId('project-123');
$this->given(new ProjectCreated($projectId, 'Project Name'))
->when(new ProjectRenamed($projectId, 'New Name'))
->then(new UpdateProjectionCommand($projectId, 'New Name'))
->assert();
}-
Test Organization
- Group tests by aggregate/listener
- Use descriptive test method names
- Follow the Given-When-Then pattern consistently
-
Test Data
- Use meaningful test data
- Create helper methods for common event patterns
- Use constants for fixed values
-
Assertions
- Verify event content, not just event types
- Check for correct command handling
- Validate state changes when relevant
-
Error Cases
- Test error conditions explicitly
- Verify error handling behavior
- Test concurrent write scenarios
class ProjectAggregateTest extends AggregateRoot\TestCase
{
private ProjectId $projectId;
private Clock $clock;
private ProjectService $projectService;
protected function setUp(): void
{
$this->projectId = new ProjectId('project-1');
$this->clock = new SystemClock();
$this->projectService = new ProjectService();
}
/**
* Required: Create and return the factory for your aggregate
*/
protected function createFactory(): Domain\AggregateRoot\Factory
{
return new ProjectAggregateFactory(
$this->clock,
$this->projectService
);
}
public function testProjectCreation(): void
{
$this->for($this->projectId)
->given() // No previous events
->when(new CreateProject($this->projectId, 'New Project'))
->then(new ProjectCreated($this->projectId, 'New Project'));
}
public function testProjectRename(): void
{
$this->for($this->projectId)
->given(new ProjectCreated($this->projectId, 'Old Name'))
->when(new RenameProject($this->projectId, 'New Name'))
->then(new ProjectRenamed($this->projectId, 'New Name'));
}
public function testCannotRenameDeactivatedProject(): void
{
$this->for($this->projectId)
->given(
new ProjectCreated($this->projectId, 'Project'),
new ProjectDeactivated($this->projectId)
)
->when(new RenameProject($this->projectId, 'New Name'))
->then(/* no events expected */);
}
}You can override these optional methods to customize testing behavior:
// Optional: Override if you need a custom command handler
protected function createHandler(
Domain\AggregateRoot\Factory $factory,
Domain\AggregateRoot\Repository $repository
): Domain\CommandHandler {
return new MyCustomHandler($repository);
// Default implementation uses AggregateRootHandler
}
// Optional: Override for custom snapshot serialization
protected function createSnapshotterSerializer(): Serializer
{
return new MyCustomSerializer();
// Default uses PhpSerializer
}
// Optional: Override for custom snapshot storage
protected function createSnapshotterStorage(): Snapshotter\Storage
{
return new MyCustomStorage();
// Default uses InMemoryStorage
}// Optional: Override for custom command handling
protected function createCommandBus(): CommandBus
{
return new MyCustomCommandBus();
// Default uses NullCommandBus
}