Skip to content

Commit

Permalink
Simple integration testing (adopted) (#16489)
Browse files Browse the repository at this point in the history
# Objective

This older PR from `Wcubed` seemed well worth saving, adopted from
#7314. See also tracking issue #2896 for ongoing discussion of Bevy
testability. Thanks `Wcubed`!

## Solution

- Updated for 0.15
- Added the `expected`/`actual` pattern
- Switched to function plugin
- Tweaked a bit of description

## Testing

Green.

---------

Co-authored-by: Wybe Westra <[email protected]>
Co-authored-by: Wybe Westra <[email protected]>
  • Loading branch information
3 people authored Nov 24, 2024
1 parent da68bfe commit bb81a2c
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs-template/EXAMPLE_README.md.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Example | Description

Example | Description
--- | ---
[How to Test Apps](../tests/how_to_test_apps.rs) | How to test apps (simple integration testing)
[How to Test Systems](../tests/how_to_test_systems.rs) | How to test systems with commands, queries or resources

# Platform-Specific Examples
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ Example | Description

Example | Description
--- | ---
[How to Test Apps](../tests/how_to_test_apps.rs) | How to test apps (simple integration testing)
[How to Test Systems](../tests/how_to_test_systems.rs) | How to test systems with commands, queries or resources

# Platform-Specific Examples
Expand Down
132 changes: 132 additions & 0 deletions tests/how_to_test_apps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//! Demonstrates simple integration testing of Bevy applications.
//!
//! By substituting [`DefaultPlugins`] with [`MinimalPlugins`], Bevy can run completely headless.
//!
//! The list of minimal plugins does not include things like window or input handling. The downside
//! of this is that resources or entities associated with those systems (for example:
//! `ButtonInput::<KeyCode>`) need to be manually added, either directly or via e.g.
//! [`InputPlugin`]. The upside, however, is that the test has complete control over these
//! resources, meaning we can fake user input, fake the window being moved around, and more.
use bevy::prelude::*;

#[derive(Component)]
struct Player {
mana: u32,
}

impl Default for Player {
fn default() -> Self {
Self { mana: 10 }
}
}

/// Splitting a Bevy project into multiple smaller plugins can make it more testable. We can
/// write tests for individual plugins in isolation, as well as for the entire project.
fn game_plugin(app: &mut App) {
app.add_systems(Startup, (spawn_player, window_title_system).chain());
app.add_systems(Update, spell_casting);
}

fn window_title_system(mut windows: Query<&mut Window>) {
for (index, mut window) in windows.iter_mut().enumerate() {
window.title = format!("This is window {index}!");
}
}

fn spawn_player(mut commands: Commands) {
commands.spawn(Player::default());
}

fn spell_casting(mut player: Query<&mut Player>, keyboard_input: Res<ButtonInput<KeyCode>>) {
if keyboard_input.just_pressed(KeyCode::Space) {
let Ok(mut player) = player.get_single_mut() else {
return;
};

if player.mana > 0 {
player.mana -= 1;
}
}
}

fn create_test_app() -> App {
let mut app = App::new();

// Note the use of `MinimalPlugins` instead of `DefaultPlugins`, as described above.
app.add_plugins(MinimalPlugins);
// Inserting a `KeyCode` input resource allows us to inject keyboard inputs, as if the user had
// pressed them.
app.insert_resource(ButtonInput::<KeyCode>::default());

// Spawning a fake window allows testing systems that require a window.
app.world_mut().spawn(Window::default());

app
}

#[test]
fn test_player_spawn() {
let mut app = create_test_app();
app.add_plugins(game_plugin);

// The `update` function needs to be called at least once for the startup
// systems to run.
app.update();

// Now that the startup systems have run, we can check if the player has
// spawned as expected.
let expected = Player::default();
let actual = app.world_mut().query::<&Player>().get_single(app.world());
assert!(actual.is_ok(), "There should be exactly 1 player.");
assert_eq!(
expected.mana,
actual.unwrap().mana,
"Player does not have expected starting mana."
);
}

#[test]
fn test_spell_casting() {
let mut app = create_test_app();
app.add_plugins(game_plugin);

// Simulate pressing space to trigger the spell casting system.
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(KeyCode::Space);
// Allow the systems to recognize the input event.
app.update();

let expected = Player::default();
let actual = app.world_mut().query::<&Player>().single(app.world());
assert_eq!(
expected.mana - 1,
actual.mana,
"A single mana point should have been used."
);

// Clear the `just_pressed` status for all `KeyCode`s
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.clear();
app.update();

// No extra spells have been cast, so no mana should have been used.
let after_keypress_event = app.world_mut().query::<&Player>().single(app.world());
assert_eq!(
expected.mana - 1,
after_keypress_event.mana,
"No further mana should have been used."
);
}

#[test]
fn test_window_title() {
let mut app = create_test_app();
app.add_plugins(game_plugin);

app.update();

let window = app.world_mut().query::<&Window>().single(app.world());
assert_eq!(window.title, "This is window 0!");
}

0 comments on commit bb81a2c

Please sign in to comment.