Rust事件溯源测试清单:用eventually-rs的given-when-then Scenario测试聚合根
【免费下载链接】eventually-rsEvent Sourcing for Rust项目地址: https://gitcode.com/gh_mirrors/ev/eventually-rs
eventually-rs 是 Rust 生态中用于构建事件溯源(Event Sourcing)应用的核心库,它不仅提供了聚合根(Aggregate Root)与事件存储的抽象,还内置了Scenario测试类型——让你用经典的given-when-then三段式画布,快速、清晰地测试聚合根的行为。本文是一份实战清单,帮你在 Rust 项目中快速上手这套事件溯源测试方法。
一、30秒背景:事件溯源与聚合根
事件溯源系统不保存数据的"当前状态",而是把所有变更按顺序存为不可变事件,当前状态由事件日志回放(rehydrate)得出。
在 eventually-rs 中:
- Aggregate(聚合):领域模型,状态只能通过领域事件变更,核心定义见
eventually/src/aggregate/mod.rs - Root:聚合根的运行时载体,负责记录未提交事件、跟踪版本号
- Scenario:专测聚合根行为的测试类型,位于
eventually/src/aggregate/test.rs
💡 理解这三者,就理解了整个测试框架:Scenario 会用 given 的事件"回放"出一个 Root,执行 when 中的操作,再断言产生的事件是否符合预期。
二、核心工具:Scenario 三段式测试
Scenario::new()开启一个场景,链式调用即可读性极强:
Scenario::<MyAggregate>::new() .given(vec![...]) // 前置条件:已有事件 .when(|root| { ... }) // 执行动作:调用聚合根方法 .then(vec![...]) // 期望结果:应产生哪些事件 .assert(); // 执行并断言前置条件:given —— 事件列表回放状态
given接收一组领域事件,Scenario 会自动将其折叠(fold)成聚合根的当前状态。这正好利用了事件溯源的本质:状态 = 事件历史。
执行动作:when —— 两种分支
| 分支 | 适用场景 | 闭包签名 |
|---|---|---|
Scenario::when | 创建全新聚合根(无历史事件) | Fn() -> Result<R, Err> |
ScenarioGiven::when | 修改已存在的聚合根 | Fn(&mut R) -> Result<(), Err> |
⚠️ 注意:given(...).when(...)分支如果给定事件无法成功回放出一个 Root,会直接 panic 并让测试失败——这是有意设计,防止测试前提本身出错。
断言结果:then / then_error / assert
.then(events):断言操作成功后恰好产生这些事件.then_error(err):断言操作按预期失败并返回指定错误.assert():执行整个场景,断言不通过则 panic 使测试失败
这套设计让你既测"快乐路径",也测"业务规则拒绝"的路径。
三、完整示例:灯开关聚合根走一遍
以官方 light-switch 示例为例(领域模型见examples/light-switch/src/domain.rs):
聚合根暴露了install(安装)、turn_on(开灯)、turn_off(关灯)等操作,并定义了AlreadyOn、NotYetInstalled等业务错误。
一个典型的场景测试思路(成功路径):
Scenario::<LightSwitch>::new() .given(vec![LightSwitchEvent::Installed(...).into()]) // 已安装 .when(|root| root.turn_on(id)) // 尝试开灯 .then(vec![LightSwitchEvent::SwitchedOn(...).into()]) // 应产生开灯事件 .assert();失败路径同样简单:
Scenario::<LightSwitch>::new() .given(vec![已安装, 已开灯 两个事件]) .when(|root| root.turn_on(id)) .then_error(LightSwitchError::AlreadyOn) // 已经开着了 .assert();🎯 两段代码就完整覆盖了"开灯成功"和"重复开灯被拒"两条业务规则,无需 mock、无需数据库。
四、进阶:命令级 Scenario(command::test::Scenario)
当你要测试的是命令处理器(Command Handler)而不仅仅是聚合根时,eventually-rs 在eventually/src/command/test.rs提供了另一套 Scenario:
command::test::Scenario .given(vec![已持久化的事件]) // 系统前置状态 .when(命令.into()) // 直接投递命令 .then(vec![期望持久化的事件]) // 或 .then_fails() .assert_on(|event_store| Service::from(repo)) .await;它与聚合级 Scenario 的关键区别:
- given 用的是
event::Persisted(含 stream_id 和 version),描述事件存储中的真实数据 - then_fails:只断言失败,不指定具体错误
- assert_on:注入一个内存事件存储构建被测服务,测试在真实(但内存中的)存储上跑完整链路
银行记账示例examples/bank-accounting/src/application.rs里就有一组完整示范:开户成功、重复开户失败、向已关闭账户存款失败、转账余额不足失败……每个场景一个测试函数,命名即文档。
五、实战清单:写 Event Sourcing 测试前对照
- 每个业务不变量至少对应一个
then_error场景(如"不能重复安装") - 每个领域事件都有一条成功路径的
then断言 - 用
given覆盖状态机边界:初始态、中间态、终态 - 聚合级用
aggregate::test::Scenario,应用级用command::test::Scenario - 测试命名遵循"场景_条件_结果"句式(参考 bank-accounting 的测试函数命名)
- 需要持久化行为时,用内存事件存储
eventually/src/event/store.rs或 PostgreSQL 实现(eventually-postgres)
六、关键文件导航
| 文件路径 | 作用 |
|---|---|
eventually/src/aggregate/mod.rs | Aggregate trait 与 Root 实现 |
eventually/src/aggregate/test.rs | 聚合级 given-when-then Scenario |
eventually/src/command/test.rs | 命令级 Scenario(then_fails / assert_on) |
eventually/src/event/store.rs | 事件存储抽象与内存实现 |
examples/light-switch/src/domain.rs | 最简聚合根领域模型示例 |
examples/bank-accounting/src/application.rs | 命令级场景测试完整示范 |
按照这份清单,你的 Rust 事件溯源测试将同时做到可读(given-when-then 即业务语言)、可信(成功与失败路径双覆盖)、可维护(每个场景独立、命名即文档)。🚀
【免费下载链接】eventually-rsEvent Sourcing for Rust项目地址: https://gitcode.com/gh_mirrors/ev/eventually-rs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考