bevy-testing
Skill29 starsChanged 11 days agoMITView on GitHub
Use when testing Bevy 0.20 with `App::update`, `TimeUpdateStrategy`, `FixedUpdate`, `MessageCursor`, or `On<E>` observers: deterministic stepping, async-task draining with `poll_once` and no sleeps, headless worlds, and capture-based visual regression.
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- bevy-a11ySkill
- bevy-animationSkill
- bevy-assetsSkill
- bevy-audioSkill
- bevy-camerasSkill
- bevy-captureSkill
- bevy-cargo-featuresSkill
- bevy-core-conceptsSkill
- bevy-custom-assetsSkill
- bevy-diagnostics-profilingSkill
- bevy-ecs-componentsSkill
- bevy-ecs-queriesSkill
- bevy-ecs-systemsSkill
- bevy-fluentSkill
- bevy-input-actionsSkill
- bevy-migration-0-17-to-0-18Skill
- bevy-migration-0-18-to-0-19Skill
- bevy-migration-0-19-to-0-20Skill
- bevy-pbr-materialsSkill
- bevy-physicsSkill
- bevy-portingSkill
- bevy-renderingSkill
- bevy-save-loadSkill
- bevySkill
- bevy-uiSkill
- bevy-vfxSkill
- bevy-voxel-dataSkill
- bevy-voxel-pipelineSkill
- bevy-voxel-runtimeSkill
- bevy-wasm-webgpuSkill
- similarity-rsSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 57
After the first clock-initialising update, FixedTimesteps(n) makes each app.update() advance by the fixed timestep times n and run exactly …
- Very specific: line 92
Read world state, messages with a test-owned MessageCursor, or observer effects.
- Distinctive: line 93
Keep deferred-command boundaries visible: a command is not applied until the schedule reaches a synchronization point.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Bevy 0.20 — deterministic testing
When to use this skill
- Systems/plugins need a minimal
Appharness and direct world assertions. - Fixed schedules, timers, messages, or observers need deterministic stepping.
- Async work must complete in tests without arbitrary sleeps.
- Rendered output needs capture-based visual regression.
Build the smallest App that owns the behaviour under test, control its clock and
inputs, step it explicitly, then inspect world state or emitted output. Do not call
App::run() in an ordinary unit/integration test.
Canonical pattern
use bevy::{prelude::*, time::{TimePlugin, TimeUpdateStrategy}};
#[derive(Resource, Default)]
struct TickCount(u32);
fn count_tick(mut count: ResMut<TickCount>) {
count.0 += 1;
}
#[test]
fn fixed_system_runs_exactly_three_times() {
let mut app = App::new();
app.add_plugins(TimePlugin)
.insert_resource(Time::<Fixed>::from_hz(60.0))
.insert_resource(TimeUpdateStrategy::FixedTimesteps(1))
.init_resource::<TickCount>()
.add_systems(FixedUpdate, count_tick);
// The first update initializes Bevy's real-time clock; it has zero delta.
app.update();
for _ in 0..3 {
app.update();
}
assert_eq!(app.world().resource::<TickCount>().0, 3);
}After the first clock-initialising update, FixedTimesteps(n) makes each
app.update() advance by the fixed timestep times n and run exactly n fixed
loops. Use ManualDuration when the test must exercise zero-tick frames, accumulated
time, or catch-up. Use ManualInstant when exact absolute instants matter and advance
its resource before each update.
Gotchas
- The first
app.update()initialises real time with zero delta; warm it up before countingFixedTimestepsticks or assert that zero-step frame deliberately. - Running
FixedUpdatedirectly bypasses fixed-main clock/message semantics. - Deferred
Commandsare invisible until a synchronization point. - Async completion must use an injected fake or a bounded predicate loop, never sleep.
DefaultPluginsadds platform/render/audio state most world tests do not need.- Observers lost the bundle generic (0.20). Stale output writes
On<Add, Health>; 0.20 isOn<Add<Health>>, and a plainOn<Hit>for custom events.IntoObserverSystemnow has three generics, so an "inscrutable" trait error usually means one generic too many. &mut Worldis an ordinarySystemParam(0.20). An exclusive test helper no longer needs a special signature;System::is_exclusiveis gone.iter_manyyieldsResult(0.20). Assertions over an entity list needquery.iter_many(ids).matched()for the old skip-missing behaviour.NextState::set_if_neqisset_if_different(0.20). The old name compiles with a deprecation warning.NameisFrom<&'static str>only (0.20). Tag test entities withName::new(string.to_owned())when the label is not a literal.- Events:
ReplacebecameDiscard(0.19);Pointer<Click>becamePointerClick(0.20). Observer tests on these names must be renamed. ScheduleBuildSettings::shuffle_seed(0.20,debugfeature) randomises unordered systems per seed. Use it in CI to surface missing.before/.after.
Test through observable contracts
- Insert resources/components and call
app.update()for scheduled behaviour. - Invoke pure helper functions directly for algorithms and state machines.
- Read world state, messages with a test-owned
MessageCursor, or observer effects. - Keep deferred-command boundaries visible: a command is not applied until the schedule reaches a synchronization point.
- Assert invariants and externally meaningful ordering, not arbitrary query/entity or same-event observer order.
Async without sleeps
Production systems poll Task<T> with block_on(poll_once(&mut task)) (both
re-exported from bevy::tasks) or bevy::tasks::futures::check_ready(&mut task).
Tests should inject a deterministic inline/fake executor when possible. Otherwise step
the app until an observable completion predicate with a strict maximum step/deadline
and fail with queue/task state. Never use "sleep 100 ms and hope the task finished."
Dropping a Bevy Task cancels it unless it was detached. Keep task ownership explicit
and ensure tests cover completion, cancellation, failure, and stale-result paths.
Headless versus rendered tests
For ECS/gameplay logic, use App::new() plus only required plugins (TimePlugin,
AssetPlugin, TaskPoolPlugin, etc.). Avoid DefaultPlugins when no window,
renderer, audio device, or filesystem is part of the contract. For render acceptance,
launch a real render app, capture a Screenshot, wait for ScreenshotCaptured, and
compare a saved artifact against a versioned baseline with documented tolerance.
Choose the relevant deep dive
| Problem | Read |
| Minimal apps, manual time, fixed loops, schedule stepping | App and time |
| Messages, observers, deferred effects, tasks and no-sleep draining | Messages, observers, and async |
| Screenshot capture, deterministic scenes, diff thresholds and artifacts | Visual regression |
Review checklist
- The test owns time; wall-clock speed cannot change its expected result.
- Each test installs only the plugins/resources the subject requires.
- Inputs and random seeds are explicit, and persistent/global state is isolated.
- Fixed input tests cover zero, one, and multiple fixed ticks per update.
- Message cursors are created before/after writes intentionally.
- Async tests have a deterministic fake or a bounded progress loop with useful failure.
- Visual failures retain actual, expected, and diff images plus run metadata.
- Baselines are updated only through an explicit reviewed operation.
See also
bevy-input-actions— frame-to-fixed input invariants.bevy-physics— Rapier headless and determinism boundaries.bevy-diagnostics-profiling— acceptance replays and performance evidence.bevy-migration-0-19-to-0-20— observer,iter_many, and state renames.- Bevy
TimeUpdateStrategy - Bevy
Task
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

