bevyengine/bevy
Testing
Bevy mixes several flavors of test. This page lays them out and tells you when to write each.
The big picture
| Layer | Where | What to put there |
|---|---|---|
| Unit tests | #[cfg(test)] mod tests {} at the bottom of each .rs |
Pure-logic tests, single-system tests, type-level checks. |
| Integration tests | tests-integration/ |
Cross-crate scenarios, full-app smoke tests. |
| Doctests | /// examples in rustdoc |
Worked examples that double as compile-checked docs. |
| Compile-fail tests | crates/<crate>/compile_fail/ |
trybuild harness checking macro error messages. |
| Example smoke tests | examples/ |
End-to-end runtime tests via tools/example-showcase. |
| Benchmarks | benches/ |
Criterion benchmarks for performance-sensitive code. |
Running tests
# Whole workspace
cargo test --workspace
# Single crate
cargo test -p bevy_ecs
# Single test file
cargo test -p bevy_ecs --test <name>
# A specific test by name substring
cargo test -p bevy_ecs change_detection
# Doctests only
cargo test --doc -p bevy_ecsThe tools/ci runner has a dedicated subcommand:
cargo run -p ci -- test
cargo run -p ci -- doc-testUnit tests
The dominant pattern. Most files in bevy_ecs, bevy_reflect, bevy_math, and bevy_app end with a #[cfg(test)] mod tests block. Tests build a small World, run a system, and assert on component values.
Typical shape:
#[cfg(test)]
mod tests {
use super::*;
use bevy_ecs::prelude::*;
#[derive(Component)]
struct Counter(u32);
#[test]
fn system_increments_counter() {
let mut world = World::new();
let id = world.spawn(Counter(0)).id();
world.run_system_once(|mut q: Query<&mut Counter>| {
q.single_mut().0 += 1;
});
assert_eq!(world.get::<Counter>(id).unwrap().0, 1);
}
}World::run_system_once and the related test helpers are designed exactly for this — see crates/bevy_ecs/src/system/system_registry.rs.
Integration tests
Live in tests-integration/. This crate is not a workspace member (intentional — it runs against the published API surface, not the workspace internals). To run it:
cd tests-integration
cargo testAdd an integration test when the test needs to use the public API the way an end user would, including plugin composition.
Doctests
Almost every public function in bevy_ecs, bevy_reflect, and bevy_math ships with a doctest. They're the canonical "how do I use this?" examples. The CI runs them via cargo run -p ci -- doc-test.
Two important patterns:
- Doctests inside
bevy_ecsimport frombevy_ecs::prelude::*, notbevy::prelude::*. The crate has nobevydependency. - Doctests that need an
AppuseApp::new()directly. UseMinimalPluginsif scheduling is needed; avoidDefaultPlugins(it requires a window).
Compile-fail tests (trybuild)
Bevy's derive macros (#[derive(Component)], #[derive(Reflect)]) need to emit good error messages when given bad input. Each macro crate has a compile_fail/ sibling that uses trybuild to lock those messages in.
cargo test -p bevy_derive --test compile_fail
cargo test -p bevy_ecs --test compile_fail
cargo test -p bevy_reflect --test compile_failThe harness compiles each .rs file in compile_fail/tests/ui/ and compares the resulting compiler output to a .stderr file. If the message changes, tests fail and you regenerate the snapshots:
TRYBUILD=overwrite cargo test -p bevy_ecs --test compile_fail(Don't blindly run that — read the diff first to make sure the change is intentional.)
Example smoke tests
Examples are first-class citizens of CI. tools/example-showcase runs every example with a window for a few seconds and compares against a recorded screenshot. The tool captures images and ships them to Pixel Eagle for visual regression.
The CI pipeline that drives this is .github/workflows/example-run.yml and .github/workflows/send-screenshots-to-pixeleagle.yml.
If you add an example, also add [[example]] metadata to the workspace Cargo.toml:
[[example]]
name = "your_example"
path = "examples/your-section/your_example.rs"
doc-scrape-examples = true
[package.metadata.example.your_example]
name = "Your Example"
description = "What this example demonstrates."
category = "Some Category"
wasm = true # whether it should also be built for the webBenchmarks
benches/ is a separate crate with Criterion benchmarks. Run with:
cargo bench -p benchesThe benches measure the hot paths in bevy_ecs (system dispatch, query iteration, change detection), bevy_reflect, and a few rendering primitives. Don't over-rely on them for absolute numbers — Criterion reports relative changes between runs, not absolute throughput.
Test patterns to copy
- Headless app:
App::new().add_plugins(MinimalPlugins).add_systems(Update, ...).run();plus anAppExitevent sender to stop the loop. - Snapshot of components: spawn entities, run schedules, assert on
world.get::<Component>(id). - Schedule introspection: use
world.resource::<Schedules>()to inspect the system graph after a plugin builds it. - Reflection round-trip: scenes are tested by serializing then deserializing and comparing components.
What not to test
- The
wgpudevice. CI doesn't have a GPU on most runners. Keep GPU-touching tests behind#[cfg(target_os = "...")]or use the example screenshot harness. - Timing. Bevy's tests deliberately avoid
std::thread::sleep— flaky tests get deleted, so write deterministic ones.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.