跳到正文

参考

测试

测试 go-sequent 应用:testkit 的聚合 Given/When/Expect、内存 store 集成测试、OCC 与幂等用例、用 GO_SEQUENT_PG_DSN 跑 Postgres 测试,以及基准测试。

测试

go-sequent 的设计让大部分测试不需要数据库。聚合是纯逻辑;内存 store 足以支撑整条写路径的集成测试。

测试层次

层次 做法
聚合单元测试 testkit Given/When/Expect
应用与集成 内存 store + 真实的 CommandService,如 bank.NewDemoApp
AuthZ、Idempotency、多租户 针对内存 store 的集成测试(仓库里的 TestIT_Memory_*)
OCC(用 ExpectedVersion 挡住并发写冲突) 用过期的 ExpectedVersion 追加
Postgres 设置了 GO_SEQUENT_PG_DSN 时运行,否则跳过

go test ./... 必须在没有 DSN 的情况下通过。

用 testkit 测聚合

testkit.For(id, factory) 从事件重建聚合,执行一个动作,并捕获结果。

func TestKitOpenAccount(t *testing.T) {
    id := uuid.New()
    s := testkit.For(id, func(id uuid.UUID) aggregate.Root {
        return bank.NewAccount(id)
    }).Given().When(func(agg aggregate.Root) error {
        return agg.(*bank.Account).OpenAccount("carol", "EUR", types.MustMoney("5.00", "EUR"))
    }).Run()

    if err := s.ExpectEvents(bank.NewAccountOpened("carol", "EUR", types.MustMoney("5.00", "EUR"))); err != nil {
        t.Fatal(err)
    }
}

带历史:

func TestKitInsufficientFunds(t *testing.T) {
    id := uuid.New()
    s := testkit.For(id, func(id uuid.UUID) aggregate.Root {
        return bank.NewAccount(id)
    }).Given(
        bank.NewAccountOpened("dave", "USD", types.MustMoney("10", "USD")),
    ).When(func(agg aggregate.Root) error {
        return agg.(*bank.Account).Withdraw(types.MustMoney("20", "USD"))
    }).Run()

    if err := s.ExpectError("insufficient funds"); err != nil {
        t.Fatal(err)
    }
}
方法 含义
Given(events...) 已有历史。自动填写聚合 id 与序号。
When(fn) 在重建后的聚合上执行的动作。
Run() 执行并捕获产生的事件与错误。
ExpectEvents(want...) 事件类型相同、顺序一致、载荷相等,忽略信封。
ExpectNoEvents() 没有错误,也没有事件。
ExpectError(substr) When 返回的错误包含 substr。
Produced()、Err() 直接读取结果。

每条业务规则都这样在聚合上测,Handler 就能保持精简。

用内存 store 做集成测试

func newApp(t *testing.T) (*sequent.App, *bank.BalanceProjector, *memory.Store) {
    app := sequent.New(sequent.Config{PublisherMode: sequent.SyncInTxn})
    bank.RegisterEvents(app.Events)
    store := memory.New(app.Events)
    store.SetSnapshotThreshold(2)
    store.SetUpcasters(app.Upcasters)
    app.Repo.Store = store
    app.Repo.Snapshots = store
    app.Repo.Publisher = app.Publisher
    app.Service.Tx = store
    // 注册 handler、aggregate、projector ...
    return app, proj, store
}

也可以用带 StoreFactory 的 bank.NewDemoApp。建议覆盖这些场景:

  • 命令 → 事件 → SyncInTxn(写事务内同步更新读模型),Execute 之后立刻读余额。
  • 从 store 重新加载聚合并检查状态。
  • Idempotency(同一业务键重复提交只生效一次)重试:同一个 (tenant, key) 执行两次只产生一次效果;也测一下 Strict。
  • AuthZ 拒绝:errors.Is(err, security.ErrDenied)。
  • 唯一键冲突:同一租户下第二个同名 owner 返回 ErrUniqueKeyViolation;跨租户则成功。
  • OCC:用过期的 ExpectedVersion 追加,期望 ErrOptimisticConcurrency。
  • 快照:设较低阈值,跑 snapshotter,然后重新加载。
  • hash chain(按聚合串联事件哈希,用来发现篡改):干净的事件流上 VerifyChain 通过。

Money 检查

func TestBanFloat64Money(t *testing.T) {
    type bad struct{ Amount float64 }
    if err := testkit.BanFloat64Money(bad{Amount: 1.23}); err == nil {
        t.Fatal("expected ban")
    }
    if err := testkit.BanFloat64Money(&bank.Deposit{Amount: types.MustMoney("1", "USD")}); err != nil {
        t.Fatal(err)
    }
}

请在自己的仓库里为每个命令和事件类型加上这样的断言。见 Money。

Postgres 测试

名为 TestIT_Postgres_* 与 TestIT_BankPG 的测试只在设置了 GO_SEQUENT_PG_DSN 时运行,否则跳过。

# 始终可跑:内存与单元测试;没有 DSN 时 Postgres 测试跳过
go test ./...
go test ./examples/bank/ ./examples/bankpg/ -count=1

# 带 Postgres
GO_SEQUENT_PG_DSN='postgres://user:pass@localhost:5432/go_sequent?sslmode=disable' \
  go test ./examples/bankpg/ -count=1 -run TestIT_BankPG

请使用一次性的数据库;测试会自行迁移 schema。

基准测试

bench 包测量追加吞吐与 OCC 冲突风暴:

go test ./bench/ -bench=. -benchmem
GO_SEQUENT_PG_DSN='postgres://…' go test ./bench/ -bench=BenchmarkPostgres -benchmem
基准 目的
BenchmarkMemoryAppendThroughput 顺序追加,每次一个新流
BenchmarkMemoryAppendSameStream 热点聚合顺序追加
BenchmarkMemoryOptimisticConflicts 并行使用过期 ExpectedVersion 产生冲突
BenchmarkMemoryConflictStormWithProgress 竞争下的正确推进加冲突计数
BenchmarkPostgresAppendThroughput PG 顺序追加(没有 DSN 时跳过)
BenchmarkPostgresOptimisticConflicts PG 冲突风暴(没有 DSN 时跳过)

基准测试需要通过 -bench 显式开启;普通 go test 不会跑它们。

CI

仓库的 GitHub Actions 工作流会跑 go mod tidy 检查、go vet、golangci-lint、不带 Postgres 的 go test -race ./...,以及针对事件库与两个 bank 示例的 Postgres 16 集成测试。不需要任何 secret。匹配 v* 的 tag 会跑同样的检查,然后用 GoReleaser 构建 CLI 的发布二进制。

相关

文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。