用途
当 Bevy crate 需要单元测试或集成测试时使用本技能。cargo test 负责运行带有 #[test] 标注的函数,而使用 MinimalPlugins 构建的无窗口 App 则是验证调度逻辑的测试载体。
主要能力
运行测试的方式
| 需求 | 命令 |
|---|---|
| 运行整套测试 | 在包目录下执行 cargo test |
| 运行指定名称 | cargo test name_filter |
| 显示测试输出 | cargo test -- --no-capture |
cargo test name_filter 会运行名称中包含该过滤字符串的测试。-- 之后的参数会传递给 libtest(例如 cargo test -- --help)。
测试执行的范围
#[test]标注的函数会被测试框架在多线程中运行。- 每个测试的工作目录是包根目录,因此无论从何处调用 Cargo,相对路径都从包根解析。
- 默认的
cargo test覆盖库、作为单元测试的二进制、集成测试与文档测试,并会构建示例。 --lib仅限库部分。目标上设置harness = false表示需要自行提供main。- 默认使用
testprofile;--profile=debug保留调试信息。
测试中的 Bevy 类型用法
在测试中构建 App,插入资源,添加系统,调用 update,然后读取 world()。通过 MinimalPlugins 避免把窗口引入测试框架。
纯逻辑可以保持为普通 #[test],无需 App。例如:
fn score_after_pickup(score: u32, value: u32) -> u32 {
score.saturating_add(value)
}
#[test]
fn pickup_adds_points() {
assert_eq!(score_after_pickup(1, 2), 3);
}
涉及调度的测试则需要一个 headless app,world() 在断言时共享使用,因此写入 Score 的查询此时已经执行完毕:
#[derive(Resource, Default)]
struct Score(u32);
fn add_point(mut score: ResMut<Score>) {
score.0 += 1;
}
#[test]
fn one_update_adds_a_point() {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.insert_resource(Score(0))
.add_systems(Update, add_point);
app.update();
assert_eq!(app.world().resource::<Score>().0, 1);
}
在 Startup 中生成实体,然后步进一次 Update;测试可以通过测试插入的资源上存储的一次性系统结果,或通过 world() 读取组件:
#[derive(Component)]
struct Marker;
fn spawn_marker(mut commands: Commands) {
commands.spawn(Marker);
}
#[test]
fn startup_spawns_one_marker() {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_systems(Startup, spawn_marker);
app.update();
let count = app.world().query::<&Marker>().iter(app.world()).count();
assert_eq!(count, 1);
}
同一组件的两个写入系统,在测试中按照游戏插件里的同一方式链式排序:
#[derive(Resource)]
struct Wallet(i32);
fn earn(mut wallet: ResMut<Wallet>) {
wallet.0 += 5;
}
fn spend(mut wallet: ResMut<Wallet>) {
wallet.0 -= 2;
}
#[test]
fn earn_then_spend() {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.insert_resource(Wallet(0))
.add_systems(Update, (earn, spend).chain());
app.update();
assert_eq!(app.world().resource::<Wallet>().0, 3);
}
状态归属与修改规则
| 部分 | 归属 | 谁可以修改 | 调度 |
|---|---|---|---|
| 单个实体上的数据 | Component |
一个系统通过 Query |
Update 或 FixedMain |
| 整个应用共享的一个值 | Resource |
一个系统 | Startup 插入,之后由读取它的调度使用 |
| 生成实体 | Commands |
执行生成的系统 | 初始世界用 Startup,运行时生成用之后的调度 |
| 不允许竞态的顺序 | .chain() |
仅被链式连接的系统 | 同一调度 |
| 插件边界 | Plugin |
build 注册系统与资源 |
在应用运行之前 |
借用规则:两个都可变查询同一组件的系统必须被排序(.chain()),或拆分到只有一个写入。不相关的系统保持并行。这是 ECS 层面的规则,而不是对某个系统集 API 的猜测。
决策参考
| 如果需要 | 这样做 | 不要这样做 |
|---|---|---|
| 逐实体数据 | Component + Query |
在 World 之外使用自由浮动的结构体 |
| 应用级配置或时钟 | Resource |
把值复制到每个实体上 |
| 稳定的模拟步进 | FixedMain 与 Fixed 时钟 |
把模拟绑定到原始帧 Update |
| 启动阶段生成 | 在 Startup 中使用 Commands |
从渲染系统生成 |
| 非共享状态的跨系统通知 | 使用 bevy-events-messaging 中的消息或观察者槽位 |
在本文件中发明新的事件类型 |
| 文档记录范围之外的 API | 先查阅官方文档 | 编造 crate 或 API |
限制与注意事项
- 上述测试载体是
MinimalPlugins、app.update()与world(),它不会捕获帧,也不注入窗口输入。 - 文档测试的工作目录规则与单元测试不同。不要假设文档测试可以像
#[test]那样读取assets/。单元测试的工作目录是包根目录。 - 本技能不提供场景树测试运行器。
- 本技能不提供帧捕获或输入注入载体。
- 本技能不使用第三方 Bevy 测试 crate,Bevy 代码保持地道的 ECS 写法,仅使用文档记录的 Bevy API。
- 不要因为两个无序系统而同时可变查询同一组件,因为除非用
.chain()排序,调度会并行运行系统。 - 不要在文档 URL 中固定
/0.20.0/,链接保持在latest,0.20 这一目标只在正文中说明一次。 - 不要在本技能中发明物理、导航、网络或 XR crate,本技能只引用文档记录的 Bevy crate 与模块。
目标版本与相关技能
目标为 Bevy 0.20+。核心 ECS 类型包括 Component、Resource、Query、Commands、Startup、Update、Fixed / FixedMain(bevy::time)、.chain()、Plugin、DefaultPlugins、MinimalPlugins。消息与观察者属于另一个职责槽位,其 API 参见 bevy-events-messaging。除非 .chain() 对冲突的系统进行排序,否则系统会并行运行。bevy-master 上的库版本固定保持 0.20。
相关技能:前置技能包括 bevy-master(说明各状态部分由谁拥有)与 bevy-project-foundations(说明 cargo test 所针对的 Cargo 包)。下游消费方暂未提供。