agentleFS
kimgoetzkekimgoetzke/coding-agent-configs/skills/rust-bevy-test-conventions/SKILL.md

rust-bevy-test-conventions

Skill2 starsChanged 7 months agoMITView on GitHub

Conventions and best practice for writing tests in Rust applications using Bevy ECS. Use when writing or creating tests for Bevy systems, components, resources, or game logic in Rust.

The file

Show its rules and text

We split this skill into the rules it gives an agent

16 rules. About 0 are common, 7 are distinctive, and 9 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈7

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 9

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Test Bevy systems using full App instances with MinimalPlugins:

    line 44
  2. Very specific:

    Create a common setup() function to reduce boilerplate:

    line 81
  3. Very specific:

    Use NextState to manually advance system states:

    line 100
  4. Very specific:

    Read from Messages<T> resource after system execution:

    line 121
  5. Distinctive:

    Test components and resources directly without full app context:

    line 145
  6. Very specific:

    Add test constructors in #[cfg(test)] blocks:

    line 182
  7. Distinctive:

    Create helper functions to abstract complex setup:

    line 211
  8. Distinctive:

    Prefer simple Rust assertions:

    line 233
  9. Distinctive:

    Use conditional compilation for feature-specific tests:

    line 243
  10. Very specific:

    Follow the pattern: <name of function/system>_<given>_<when>_<then>

    line 333
  11. Very specific:

    Only use _<given> when <name of function/system> and <when> are the same

    line 335
  12. Very specific:

    Minimal mocking: Prefer real components with MinimalPlugins over mocks

    line 348
  13. Distinctive:

    Descriptive names: Test names should read as specifications

    line 351
  14. Distinctive:

    Inline documentation: Add comments explaining setup, action, and verification phases

    line 352
  15. Very specific:

    Fast tests: Use MinimalPlugins instead of DefaultPlugins for speed

    line 353
  16. Distinctive:

    Clear assertions: Verify specific values, not just "truthy" conditions

    line 355

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust Bevy test conventions

Quick start

#[cfg(test)]
mod tests {
    use super::*;
    use bevy::prelude::*;
    use bevy::MinimalPlugins;

    fn setup() -> App {
        let mut app = App::new();
        app.add_plugins((MinimalPlugins, YourPlugin));
        app
    }

    #[test]
    fn system_produces_expected_output() {
        let mut app = setup();

        // Setup: Add resources and entities
        app.insert_resource(YourResource::default());

        // Action: Run the system
        app.update();

        // Assert: Verify results
        let resource = app.world().resource::<YourResource>();
        assert_eq!(resource.value, expected_value);
    }
}

Testing ECS systems

Integration-style approach

Test Bevy systems using full App instances with MinimalPlugins:

fn setup() -> App {
    let mut app = App::new();
    app.add_plugins((
        MinimalPlugins,
        YourPlugin,
        // Add other required plugins
    ));
    app
}

#[test]
fn player_input_system_sends_move_message() {
    let mut app = setup();

    // Setup state
    app.insert_resource(NextState::Pending(AppState::Playing));
    app.update(); // Apply state transition

    // Trigger system behavior
    app.world_mut().send_event(KeyboardInput {
        key_code: KeyCode::KeyW,
        state: ButtonState::Pressed,
        /* ... */
    });
    app.update();

    // Verify via message resource
    let messages = app.world().resource::<Messages<MoveMessage>>();
    assert!(!messages.is_empty());
}

Setup helper functions

Create a common setup() function to reduce boilerplate:

fn setup() -> App {
    let mut app = App::new();
    app.add_plugins((MinimalPlugins, ControlsPlugin, StatesPlugin));
    app.init_resource::<Messages<YourMessage>>();
    app
}

fn setup_with_player() -> App {
    let mut app = setup();
    app.world_mut().spawn(Player::default());
    app
}

State simulation

Use NextState to manually advance system states:

fn change_app_state(app: &mut App, state: AppState) {
    app.insert_resource(NextState::Pending(state));
    app.update(); // Process state transition
}

#[test]
fn system_only_runs_in_playing_state() {
    let mut app = setup();
    change_app_state(&mut app, AppState::Playing);

    // Now test system behavior
    app.update();
    // ...
}

Message-based verification

Read from Messages<T> resource after system execution:

#[test]
fn action_system_sends_action_message() {
    let mut app = setup();

    // Trigger action
    send_action_input(&mut app);
    app.update();

    // Verify message was sent
    let messages = app.world().resource::<Messages<ActionMessage>>();
    assert_eq!(messages.len(), 1);

    let message = &messages[0];
    assert_eq!(message.action_type, ActionType::Jump);
}

Testing components & resources

Unit test pattern

Test components and resources directly without full app context:

#[test]
fn snake_segment_default_is_empty() {
    let segment = SnakeSegment::default();
    assert!(segment.positions().is_empty());
    assert!(segment.mesh_entity().is_none());
}

#[test]
fn player_health_decreases_on_damage() {
    let mut health = Health::new(100);
    health.take_damage(25);
    assert_eq!(health.current(), 75);
}

Behavior testing

Exercise component methods and verify state changes:

#[test]
fn inventory_add_item_increases_count() {
    let mut inventory = Inventory::default();
    inventory.add_item(Item::Sword);

    assert_eq!(inventory.item_count(), 1);
    assert!(inventory.contains(&Item::Sword));
}

Test utilities & helpers

Test-only constructor methods

Add test constructors in #[cfg(test)] blocks:

impl PlayerInput {
    #[cfg(test)]
    pub(crate) fn test(id: u8, direction: Direction) -> Self {
        Self {
            player_id: PlayerId(id),
            direction,
            timestamp: 0,
        }
    }
}

impl RegisteredPlayer {
    #[cfg(test)]
    pub fn new_immutable(id: PlayerId, name: String) -> Self {
        Self {
            id,
            name,
            is_mutable: false,
            connection: None,
        }
    }
}

Test helper functions

Create helper functions to abstract complex setup:

fn handle_key_input(app: &mut App, desired_input: TestKeyboardInput) {
    let mut keyboard_input = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
    match desired_input {
        TestKeyboardInput::Press(key_code) => keyboard_input.press(key_code),
        TestKeyboardInput::Release(key_code) => keyboard_input.release(key_code),
    };
    app.update();
}

enum TestKeyboardInput {
    Press(KeyCode),
    Release(KeyCode),
}

Common patterns & conventions

Assertion-first testing

Prefer simple Rust assertions:

assert!(condition, "Helpful, succinct failure message");
assert_eq!(actual, expected);
assert_ne!(actual, unexpected);

Feature-gated tests

Use conditional compilation for feature-specific tests:

#[cfg(feature = "online")]
#[test]
fn multiplayer_connection_establishes() {
    // Test only runs when 'online' feature is enabled
}

Explicit failure messages

Provide clear panic messages for unexpected states:

let Some(entity) = entities.first() else {
    panic!("Expected at least one entity but found none");
};

match result {
    Ok(value) => assert_eq!(value, expected),
    Err(e) => panic!("Expected Ok but got Err: {:?}", e),
}

Resource lifecycle

Drop mutated resources before reading to ensure cleanup:

{
    let mut resource = app.world_mut().resource_mut::<YourResource>();
    resource.modify();
} // Drop mutable borrow

let resource = app.world().resource::<YourResource>();
assert_eq!(resource.state, ExpectedState);

Working with messages

  • Since Bevy 0.17, you will normally use Messages where you previously used Events
  • app.world().send_event() does not exist - use .write_message() instead
// Write a message
app
    .world_mut()
    .write_message(YourMessage::Something(PlayerId(0)))
    .expect("Failed to write YourMessage message");


// Read produced messages
let messages = app
    .world_mut()
    .get_resource_mut::<Messages<YourMessage>>()
    .expect("YourMessage has not been sent");

// Match on specifics
let has_something = messages
    .iter_current_update_messages()
    .any(|ym| matches!(ym, YourMessage::Something(_)));
assert!(has_something, "Expected 'Something' to have been sent");

// Match message was sent
let mut messages: Mut<Messages<YourMessage>> = app
    .world_mut()
    .get_resource_mut::<Messages<YourMessage>>()
    .expect("YourMessage has not been sent");
assert!(messages.drain().next().is_some(), "YourMessage was never sent");

Changing app state

let mut app = setup();

let state = app.world().resource::<State<AppState>>();
assert_eq!(state.get(), &AppState::Loading);

let mut next_state = app.world_mut().resource_mut::<NextState<AppState>>();
next_state.set(AppState::Registering);
app.update();

let state = app.world().resource::<State<AppState>>();
assert_eq!(state.get(), &AppState::Registering);

Naming conventions

Follow the pattern: <name of function/system>_<given>_<when>_<then>

  • Only use _<given> when <name of function/system> and <when> are the same
  • <given>, <when> and <then> are silent - don't write out these words
  • Use snake case

Good examples:

  • snake_segment_default_is_empty when testing SnakeSegment::default()
  • player_input_system_sends_move_and_action_messages when testing fn player_input_system
  • register_adds_player_when_not_already_registered when testing {registered_players}.register({...})
  • unregister_mutable_returns_error_when_player_is_remote when testing {registered_players}.unregister_mutable({...})

Best practices

  1. Minimal mocking: Prefer real components with MinimalPlugins over mocks
  2. DRY setup: Extract common setup into helper functions
  3. Test one behavior: Each test should verify a single behavior
  4. Descriptive names: Test names should read as specifications
  5. Inline documentation: Add comments explaining setup, action, and verification phases
  6. Fast tests: Use MinimalPlugins instead of DefaultPlugins for speed
  7. Isolation: Tests should not depend on execution order
  8. Clear assertions: Verify specific values, not just "truthy" conditions

Advanced patterns

Testing systems with queries

#[test]
fn damage_system_applies_to_all_enemies() {
    let mut app = setup();

    // Spawn multiple entities
    app.world_mut().spawn((Enemy, Health::new(100)));
    app.world_mut().spawn((Enemy, Health::new(100)));
    app.world_mut().spawn((Enemy, Health::new(100)));

    app.insert_resource(GlobalDamage(10));
    app.update();

    // Query all enemies
    let mut query = app.world_mut().query::<&Health>();
    for health in query.iter(app.world()) {
        assert_eq!(health.current(), 90);
    }
}

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.