Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
The single greatest practical benefit of CQRS is that it makes testing possible at the right granularity. A monolithic service with one model forces you to load the entire application context — database, cache, event bus — to test a single business rule. CQRS separates concerns so cleanly that you can test a command handler with a mock repository in under a millisecond, test a projection by replaying a list of events without any infrastructure, and test the read path by seeding a fake Redis and calling the query directly. This granularity means your test suite catches regressions at the exact layer where the regression occurred, rather than through a full integration test that takes 10 seconds to fail.
Testing a CreatePost command handler and a feed projection entirely without real databases — pure in-memory unit tests.
Use these three in order. Each builds on the one before.
In one paragraph, explain why CQRS makes unit testing easier compared to a traditional service that reads and writes through the same model.
Walk me through how to test a command handler at unit speed without a real database — what the mock repository provides, what the test asserts, and what infrastructure is never started.
Given a projection that depends on events from two different aggregates (e.g., a user-renamed event must update all feed items for that user), how would you test the cross-aggregate projection logic without starting any infrastructure?
package main
import (
"errors"
"fmt"
"testing"
)
// --- Command handler under test ---
type CreatePostCmd2 struct{ Title, AuthorID string }
func (c CreatePostCmd2) Validate() error {
if c.Title == "" { return errors.New("title required") }
return nil
}
type SavedPost struct{ Title, AuthorID string }
type MockPostRepo struct{ saved []SavedPost; failOnSave bool }
func (m *MockPostRepo) Save(p SavedPost) error {
if m.failOnSave { return errors.New("db error") }
m.saved = append(m.saved, p)
return nil
}
func handleCreatePost2(cmd CreatePostCmd2, repo *MockPostRepo) error {
if err := cmd.Validate(); err != nil { return err }
return repo.Save(SavedPost{Title: cmd.Title, AuthorID: cmd.AuthorID})
}
// --- Projection under test ---
type Event2 struct{ Type, Tag, Title string }
type FeedProjection struct{ Title string; Tags []string }
func replayFeed(events []Event2) FeedProjection {
var p FeedProjection
for _, e := range events {
if e.Type == "PostCreated" { p.Title = e.Title }
if e.Type == "TagAdded" { p.Tags = append(p.Tags, e.Tag) }
}
return p
}
// --- Tests ---
func TestCreatePost_valid(t *testing.T) {
repo := &MockPostRepo{}
err := handleCreatePost2(CreatePostCmd2{Title: "Hello", AuthorID: "u1"}, repo)
if err != nil { t.Fatalf("unexpected error: %v", err) }
if len(repo.saved) != 1 { t.Fatalf("expected 1 saved post, got %d", len(repo.saved)) }
fmt.Println("PASS: valid command saves to repo")
}
func TestCreatePost_emptyTitle(t *testing.T) {
repo := &MockPostRepo{}
err := handleCreatePost2(CreatePostCmd2{Title: "", AuthorID: "u1"}, repo)
if err == nil { t.Fatal("expected validation error") }
if len(repo.saved) != 0 { t.Fatal("repo should not be called on invalid command") }
fmt.Println("PASS: empty title rejected before repo touch")
}
func TestFeedProjection_replay(t *testing.T) {
events := []Event2{
{Type: "PostCreated", Title: "CQRS Deep Dive"},
{Type: "TagAdded", Tag: "go"},
{Type: "TagAdded", Tag: "arch"},
}
proj := replayFeed(events)
if proj.Title != "CQRS Deep Dive" { t.Fatalf("wrong title: %s", proj.Title) }
if len(proj.Tags) != 2 { t.Fatalf("expected 2 tags, got %d", len(proj.Tags)) }
fmt.Println("PASS: projection replayed correctly from 3 events")
}
func main() {
t := &testing.T{}
TestCreatePost_valid(t)
TestCreatePost_emptyTitle(t)
TestFeedProjection_replay(t)
}go run main.go