SuperAPI AI 工具集

bevy-testing-patterns

测试与质量

查看项目

更新于 10-16 11:40

技能介绍

用途

当 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。
  • 默认使用 test profile;--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 包)。下游消费方暂未提供。