How to test event-sourced code¶
Test commands, queries, and projectors using the in-memory store and event bus.
Testing the decide function directly¶
decide is a pure function — test it without any infrastructure:
package createtask_test
import (
"testing"
"github.com/google/uuid"
// import your package
)
func TestDecide_NewTask(t *testing.T) {
state := taskState{Exists: false}
cmd := CreateTask{TaskID: uuid.New(), Title: "Test task"}
evts, err := decide(state, cmd)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(evts) != 1 {
t.Fatalf("expected 1 event, got %d", len(evts))
}
created, ok := evts[0].(*events.TaskCreated)
if !ok {
t.Fatal("expected TaskCreated")
}
if created.Title != "Test task" {
t.Errorf("expected title 'Test task', got %q", created.Title)
}
}
func TestDecide_DuplicateTask(t *testing.T) {
state := taskState{Exists: true}
cmd := CreateTask{TaskID: uuid.New(), Title: "Duplicate"}
_, err := decide(state, cmd)
if err == nil {
t.Fatal("expected error for duplicate task")
}
}
Testing a command handler end-to-end¶
Use the in-memory store as a real store:
package createtask_test
import (
"context"
"testing"
"github.com/google/uuid"
memstore "github.com/terraskye/eventsourcing/eventstore/memory"
"your-module/slices/createtask"
)
func TestCreateTask_Success(t *testing.T) {
store := memstore.NewMemoryStore(10)
defer store.Close()
handler := createtask.NewHandler(store)
cmd := createtask.CreateTask{
TaskID: uuid.New(),
Title: "Test task",
}
result, err := handler(context.Background(), cmd)
if err != nil {
t.Fatal(err)
}
if !result.Successful {
t.Error("expected successful result")
}
if result.NextExpectedVersion != 1 {
t.Errorf("expected version 1, got %d", result.NextExpectedVersion)
}
}
func TestCreateTask_DuplicateID(t *testing.T) {
store := memstore.NewMemoryStore(10)
defer store.Close()
handler := createtask.NewHandler(store)
cmd := createtask.CreateTask{TaskID: uuid.New(), Title: "Task"}
// First call succeeds
if _, err := handler(context.Background(), cmd); err != nil {
t.Fatal(err)
}
// Second call with the same ID fails
_, err := handler(context.Background(), cmd)
if err == nil {
t.Fatal("expected error on duplicate")
}
}
Testing business rule violations¶
Check that the error is a ErrBusinessRuleViolation:
import (
"errors"
"github.com/terraskye/eventsourcing"
)
func TestCompleteTask_NotExist(t *testing.T) {
store := memstore.NewMemoryStore(10)
defer store.Close()
handler := completetask.NewHandler(store)
cmd := completetask.CompleteTask{TaskID: uuid.New()}
_, err := handler(context.Background(), cmd)
var violation *eventsourcing.ErrBusinessRuleViolation
if !errors.As(err, &violation) {
t.Fatalf("expected ErrBusinessRuleViolation, got %T: %v", err, err)
}
}
Testing a query handler¶
func TestListTasks_Empty(t *testing.T) {
store := memstore.NewMemoryStore(10)
defer store.Close()
qh := listtasks.NewQueryHandler(store)
result, err := qh.HandleQuery(context.Background(), listtasks.ListTasks{})
if err != nil {
t.Fatal(err)
}
if len(result.Tasks) != 0 {
t.Errorf("expected 0 tasks, got %d", len(result.Tasks))
}
}
func TestListTasks_AfterCreation(t *testing.T) {
store := memstore.NewMemoryStore(10)
defer store.Close()
// Create a task via command handler (exercises real store writes)
createHandler := createtask.NewHandler(store)
_, _ = createHandler(context.Background(), createtask.CreateTask{
TaskID: uuid.New(),
Title: "My task",
})
qh := listtasks.NewQueryHandler(store)
result, _ := qh.HandleQuery(context.Background(), listtasks.ListTasks{})
if len(result.Tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(result.Tasks))
}
if result.Tasks[0].Title != "My task" {
t.Errorf("unexpected title: %q", result.Tasks[0].Title)
}
}
Testing a projector¶
import (
"context"
membus "github.com/terraskye/eventsourcing/eventbus/memory"
)
func TestProjector_OnTaskCreated(t *testing.T) {
projector := tasklist.NewProjector()
_ = projector.OnTaskCreated(context.Background(), &events.TaskCreated{
TaskID: uuid.New(),
Title: "Projected task",
})
tasks := projector.All()
if len(tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(tasks))
}
if tasks[0].Title != "Projected task" {
t.Errorf("unexpected title: %q", tasks[0].Title)
}
}
Tips¶
- Test
decideandevolvein isolation — they are pure functions and require no infrastructure. - Use
memstore.NewMemoryStorefor integration tests that need a real store. - Use
membus.NewEventBusfor tests that involve event bus subscriptions. - Close stores and buses with
defer store.Close()/defer bus.Close()to avoid goroutine leaks in tests. - Avoid global state — the event registry uses package-level globals; register events in
TestMainif needed.