Open-Source Wikis

/

Bevy

/

How to contribute

/

Testing

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_ecs

The tools/ci runner has a dedicated subcommand:

cargo run -p ci -- test
cargo run -p ci -- doc-test

Unit 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 test

Add 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_ecs import from bevy_ecs::prelude::*, not bevy::prelude::*. The crate has no bevy dependency.
  • Doctests that need an App use App::new() directly. Use MinimalPlugins if scheduling is needed; avoid DefaultPlugins (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_fail

The 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 web

Benchmarks

benches/ is a separate crate with Criterion benchmarks. Run with:

cargo bench -p benches

The 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 an AppExit event 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 wgpu device. 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.

Testing – Bevy wiki | Factory