From d27ee176d7f0cd7281e5e758a7b8af5adbd07def Mon Sep 17 00:00:00 2001 From: Wybe Westra Date: Sat, 21 Jan 2023 14:54:48 +0100 Subject: [PATCH 1/3] Show basic application testing (#2896) --- tests/how_to_test_full_app.rs | 113 ++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 tests/how_to_test_full_app.rs diff --git a/tests/how_to_test_full_app.rs b/tests/how_to_test_full_app.rs new file mode 100644 index 0000000000000..eaea00c84fbb1 --- /dev/null +++ b/tests/how_to_test_full_app.rs @@ -0,0 +1,113 @@ +use bevy::prelude::*; + +const DEFAULT_MANA: u32 = 10; + +#[derive(Component)] +struct Player { + mana: u32, +} + +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 { mana: DEFAULT_MANA }); +} + +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 how we use `MinimalPlugins` instead of `DefaultPlugins`. + // This is what allows the test to run without a window, or real user input. + 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(Input::::default()); + + // Spawning a fake window allows testing systems that require a window. + app.world.spawn(Window::default()); + + app +} + +fn add_game_systems(app: &mut App) { + // This could be a subset of your game's systems, or the entire app. + // As long as you make sure to add a fake version of inputs, windows, and any + // other things that your game's systems rely on. + app.add_startup_system(spawn_player) + .add_startup_system(window_title_system) + .add_system(spell_casting); +} + +#[test] +fn test_player_spawn() { + let mut app = create_test_app(); + add_game_systems(&mut app); + + // 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 player = app.world.query::<&Player>().get_single(&app.world); + assert!(player.is_ok(), "There should be exactly 1 player."); + assert_eq!( + player.unwrap().mana, + DEFAULT_MANA, + "Player does not have expected starting mana." + ); +} + +#[test] +fn test_spell_casting() { + let mut app = create_test_app(); + add_game_systems(&mut app); + + // We simulate pressing `space` to trigger the spell casting system. + app.world + .resource_mut::>() + .press(KeyCode::Space); + // Allow the systems to realize space got pressed. + app.update(); + + // The spell casting should have used up a single mana. + let player = app.world.query::<&Player>().single(&app.world); + assert_eq!(player.mana, DEFAULT_MANA - 1); + + // Clear the `just_pressed` status for all `KeyCode`s + app.world.resource_mut::>().clear(); + app.update(); + + // No extra spells should have been cast, so no mana should have been lost. + let player = app.world.query::<&Player>().single(&app.world); + assert_eq!(player.mana, DEFAULT_MANA - 1); +} + +#[test] +fn test_faking_windows() { + let mut app = create_test_app(); + add_game_systems(&mut app); + + // The `update` function needs to be called at least once for the startup + // systems to run. + app.update(); + + let window = app.world.query::<&Window>().single(&app.world); + assert_eq!(window.title, "This is window 0!"); +} From 00c9492021703fbd507f3e3d2e07ae31e3500998 Mon Sep 17 00:00:00 2001 From: Wybe Westra Date: Sat, 21 Jan 2023 21:54:06 +0100 Subject: [PATCH 2/3] Show that the application-under-test can be added as a plugin. Add module level documentation. --- tests/how_to_test_full_app.rs | 43 +++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/tests/how_to_test_full_app.rs b/tests/how_to_test_full_app.rs index eaea00c84fbb1..1cb1552232e5d 100644 --- a/tests/how_to_test_full_app.rs +++ b/tests/how_to_test_full_app.rs @@ -1,3 +1,17 @@ +//! This module serves as an example, and test-case, for how to automatically test full bevy apps. +//! +//! The way it works, is by substituting the [`DefaultPlugins`] with [`MinimalPlugins`], which +//! allows bevy to run completely headless. +//! +//! The list of minimal plugins does not include things like window or input handling. +//! This has as downside that the resources / entities associated with those systems +//! (for example: `Input::`) need to be manually added. +//! 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. +//! +//! The benefit of having this set of example tests be run by the CI system, is that we ensure +//! these full-app tests keep working during bevy's development. + use bevy::prelude::*; const DEFAULT_MANA: u32 = 10; @@ -29,6 +43,20 @@ fn spell_casting(mut player: Query<&mut Player>, keyboard_input: Res App { let mut app = App::new(); @@ -45,19 +73,10 @@ fn create_test_app() -> App { app } -fn add_game_systems(app: &mut App) { - // This could be a subset of your game's systems, or the entire app. - // As long as you make sure to add a fake version of inputs, windows, and any - // other things that your game's systems rely on. - app.add_startup_system(spawn_player) - .add_startup_system(window_title_system) - .add_system(spell_casting); -} - #[test] fn test_player_spawn() { let mut app = create_test_app(); - add_game_systems(&mut app); + app.add_plugin(GamePlugin); // The `update` function needs to be called at least once for the startup // systems to run. @@ -77,7 +96,7 @@ fn test_player_spawn() { #[test] fn test_spell_casting() { let mut app = create_test_app(); - add_game_systems(&mut app); + app.add_plugin(GamePlugin); // We simulate pressing `space` to trigger the spell casting system. app.world @@ -102,7 +121,7 @@ fn test_spell_casting() { #[test] fn test_faking_windows() { let mut app = create_test_app(); - add_game_systems(&mut app); + app.add_plugin(GamePlugin); // The `update` function needs to be called at least once for the startup // systems to run. From 839d7d29504ccbae8cdfe0027b07786b6bb46be9 Mon Sep 17 00:00:00 2001 From: Wybe Westra Date: Sun, 22 Jan 2023 12:40:29 +0100 Subject: [PATCH 3/3] Update module level docs according to review comments. --- tests/how_to_test_full_app.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/how_to_test_full_app.rs b/tests/how_to_test_full_app.rs index 1cb1552232e5d..79f904570da8d 100644 --- a/tests/how_to_test_full_app.rs +++ b/tests/how_to_test_full_app.rs @@ -1,16 +1,12 @@ //! This module serves as an example, and test-case, for how to automatically test full bevy apps. //! -//! The way it works, is by substituting the [`DefaultPlugins`] with [`MinimalPlugins`], which -//! allows bevy to run completely headless. +//! By substituting [`DefaultPlugins`] with [`MinimalPlugins`], Bevy can run completely headless. //! //! The list of minimal plugins does not include things like window or input handling. //! This has as downside that the resources / entities associated with those systems -//! (for example: `Input::`) need to be manually added. +//! (for example: `Input::`) 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. -//! -//! The benefit of having this set of example tests be run by the CI system, is that we ensure -//! these full-app tests keep working during bevy's development. use bevy::prelude::*;