diff --git a/docs-template/EXAMPLE_README.md.tpl b/docs-template/EXAMPLE_README.md.tpl index c1fbfc08eb153..94e7c07dab5e0 100644 --- a/docs-template/EXAMPLE_README.md.tpl +++ b/docs-template/EXAMPLE_README.md.tpl @@ -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 diff --git a/examples/README.md b/examples/README.md index 92bf3f188be1b..0027df377af2c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/tests/how_to_test_apps.rs b/tests/how_to_test_apps.rs new file mode 100644 index 0000000000000..441c81082a80f --- /dev/null +++ b/tests/how_to_test_apps.rs @@ -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::`) 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>) { + 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::::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::>() + .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::>() + .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!"); +}