mcpField
B
bevy-testing

bevy-testing

github.com/chrisgliddonTesting & quality2666 installsUpdated 9w ago
Repository

About bevy-testing

bevy-testing is a deterministic testing skill for Bevy 0.19 developers testing ECS behavior, time, async tasks, or rendered output. It focuses on controlling clocks and inputs in tests, then checking observable state or captured images.

Key features

  1. 01Build minimal App test harnesses
  2. 02Control time and step fixed schedules
  3. 03Inspect world state, messages, and observer effects
  4. 04Test async work without sleeps
  5. 05Capture rendered output for visual regression

Use cases

  1. 01Verify fixed schedules and timer behavior
  2. 02Test messages, observers, and deferred commands
  3. 03Check async task completion deterministically
  4. 04Compare render screenshots with baselines

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/chrisgliddon/bevy-skills --skill bevy-testing

After installing, type/bevy-testingin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
app-and-time.md
messages-observers-and-async.md
visual-regression.md

Details

From the project README

Bevy 0.19 — deterministic testing

When to use this skill

  • Systems/plugins need a minimal App harness 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 counting FixedTimesteps ticks or assert that zero-step frame deliberately.
  • Running FixedUpdate directly bypasses fixed-main clock/message semantics.
  • Deferred Commands are invisible until a synchronization point.
  • Async completion must use an injected fake or a bounded predicate loop, never sleep.
  • DefaultPlugins adds platform/render/audio state most world tests do not need.

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 should poll Task<T> with bevy::tasks::futures::check_ready, or drain a result channel non-blockingly. 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, 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

ProblemRead
Minimal apps, manual time, fixed loops, schedule steppingApp and time
Messages, observers, deferred effects, tasks and no-sleep drainingMessages, observers, and async
Screenshot capture, deterministic scenes, diff thresholds and artifactsVisual 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

Comments

Markdown supported