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.
In this repository23 other files it gives its agents.
- .claude/CLAUDE.mdCLAUDE.md
- .copilot/copilot-instructions.mdCopilot instructions
- ahaSkill
- explain-prSkill
- fetch-planSkill
- generate-pr-descriptionSkill
- grill-meSkill
- improve-a-planSkill
- improve-codebaseSkill
- persistSkill
- planning-modeSkill
- planningSkill
- research-modeSkill
- review-prSkill
- rust-bevy-standardsSkill
- rust-standardsSkill
- tddSkill
- unslopSkill
- update-agentsSkill
- update-planSkill
- update-skillsSkill
- write-a-skillSkill
- wtfSkill
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
- Very specific: line 44
Test Bevy systems using full App instances with MinimalPlugins:
- Very specific: line 81
Create a common setup() function to reduce boilerplate:
- Very specific: line 100
Use NextState to manually advance system states:
- Very specific: line 121
Read from Messages<T> resource after system execution:
- Distinctive: line 145
Test components and resources directly without full app context:
- Very specific: line 182
Add test constructors in #[cfg(test)] blocks:
- Distinctive: line 211
Create helper functions to abstract complex setup:
- Distinctive: line 233
Prefer simple Rust assertions:
- Distinctive: line 243
Use conditional compilation for feature-specific tests:
- Very specific: line 333
Follow the pattern: <name of function/system>_<given>_<when>_<then>
- Very specific: line 335
Only use _<given> when <name of function/system> and <when> are the same
- Very specific: line 348
Minimal mocking: Prefer real components with MinimalPlugins over mocks
- Distinctive: line 351
Descriptive names: Test names should read as specifications
- Distinctive: line 352
Inline documentation: Add comments explaining setup, action, and verification phases
- Very specific: line 353
Fast tests: Use MinimalPlugins instead of DefaultPlugins for speed
- Distinctive: line 355
Clear assertions: Verify specific values, not just "truthy" conditions
≈ “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
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 usedEvents 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_emptywhen testingSnakeSegment::default()player_input_system_sends_move_and_action_messageswhen testingfn player_input_systemregister_adds_player_when_not_already_registeredwhen testing{registered_players}.register({...})unregister_mutable_returns_error_when_player_is_remotewhen testing{registered_players}.unregister_mutable({...})
Best practices
- Minimal mocking: Prefer real components with
MinimalPluginsover mocks - DRY setup: Extract common setup into helper functions
- Test one behavior: Each test should verify a single behavior
- Descriptive names: Test names should read as specifications
- Inline documentation: Add comments explaining setup, action, and verification phases
- Fast tests: Use
MinimalPluginsinstead ofDefaultPluginsfor speed - Isolation: Tests should not depend on execution order
- 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);
}
}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.

