Skip to content

Commit bb81a2c

Browse files
bas-ieWcubed
andauthored
Simple integration testing (adopted) (#16489)
# 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]>
1 parent da68bfe commit bb81a2c

File tree

3 files changed

+134
-0
lines changed

3 files changed

+134
-0
lines changed

docs-template/EXAMPLE_README.md.tpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Example | Description
8282

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

8788
# Platform-Specific Examples

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,7 @@ Example | Description
547547

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

552553
# Platform-Specific Examples

tests/how_to_test_apps.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//! Demonstrates simple integration testing of Bevy applications.
2+
//!
3+
//! By substituting [`DefaultPlugins`] with [`MinimalPlugins`], Bevy can run completely headless.
4+
//!
5+
//! The list of minimal plugins does not include things like window or input handling. The downside
6+
//! of this is that resources or entities associated with those systems (for example:
7+
//! `ButtonInput::<KeyCode>`) need to be manually added, either directly or via e.g.
8+
//! [`InputPlugin`]. The upside, however, is that the test has complete control over these
9+
//! resources, meaning we can fake user input, fake the window being moved around, and more.
10+
use bevy::prelude::*;
11+
12+
#[derive(Component)]
13+
struct Player {
14+
mana: u32,
15+
}
16+
17+
impl Default for Player {
18+
fn default() -> Self {
19+
Self { mana: 10 }
20+
}
21+
}
22+
23+
/// Splitting a Bevy project into multiple smaller plugins can make it more testable. We can
24+
/// write tests for individual plugins in isolation, as well as for the entire project.
25+
fn game_plugin(app: &mut App) {
26+
app.add_systems(Startup, (spawn_player, window_title_system).chain());
27+
app.add_systems(Update, spell_casting);
28+
}
29+
30+
fn window_title_system(mut windows: Query<&mut Window>) {
31+
for (index, mut window) in windows.iter_mut().enumerate() {
32+
window.title = format!("This is window {index}!");
33+
}
34+
}
35+
36+
fn spawn_player(mut commands: Commands) {
37+
commands.spawn(Player::default());
38+
}
39+
40+
fn spell_casting(mut player: Query<&mut Player>, keyboard_input: Res<ButtonInput<KeyCode>>) {
41+
if keyboard_input.just_pressed(KeyCode::Space) {
42+
let Ok(mut player) = player.get_single_mut() else {
43+
return;
44+
};
45+
46+
if player.mana > 0 {
47+
player.mana -= 1;
48+
}
49+
}
50+
}
51+
52+
fn create_test_app() -> App {
53+
let mut app = App::new();
54+
55+
// Note the use of `MinimalPlugins` instead of `DefaultPlugins`, as described above.
56+
app.add_plugins(MinimalPlugins);
57+
// Inserting a `KeyCode` input resource allows us to inject keyboard inputs, as if the user had
58+
// pressed them.
59+
app.insert_resource(ButtonInput::<KeyCode>::default());
60+
61+
// Spawning a fake window allows testing systems that require a window.
62+
app.world_mut().spawn(Window::default());
63+
64+
app
65+
}
66+
67+
#[test]
68+
fn test_player_spawn() {
69+
let mut app = create_test_app();
70+
app.add_plugins(game_plugin);
71+
72+
// The `update` function needs to be called at least once for the startup
73+
// systems to run.
74+
app.update();
75+
76+
// Now that the startup systems have run, we can check if the player has
77+
// spawned as expected.
78+
let expected = Player::default();
79+
let actual = app.world_mut().query::<&Player>().get_single(app.world());
80+
assert!(actual.is_ok(), "There should be exactly 1 player.");
81+
assert_eq!(
82+
expected.mana,
83+
actual.unwrap().mana,
84+
"Player does not have expected starting mana."
85+
);
86+
}
87+
88+
#[test]
89+
fn test_spell_casting() {
90+
let mut app = create_test_app();
91+
app.add_plugins(game_plugin);
92+
93+
// Simulate pressing space to trigger the spell casting system.
94+
app.world_mut()
95+
.resource_mut::<ButtonInput<KeyCode>>()
96+
.press(KeyCode::Space);
97+
// Allow the systems to recognize the input event.
98+
app.update();
99+
100+
let expected = Player::default();
101+
let actual = app.world_mut().query::<&Player>().single(app.world());
102+
assert_eq!(
103+
expected.mana - 1,
104+
actual.mana,
105+
"A single mana point should have been used."
106+
);
107+
108+
// Clear the `just_pressed` status for all `KeyCode`s
109+
app.world_mut()
110+
.resource_mut::<ButtonInput<KeyCode>>()
111+
.clear();
112+
app.update();
113+
114+
// No extra spells have been cast, so no mana should have been used.
115+
let after_keypress_event = app.world_mut().query::<&Player>().single(app.world());
116+
assert_eq!(
117+
expected.mana - 1,
118+
after_keypress_event.mana,
119+
"No further mana should have been used."
120+
);
121+
}
122+
123+
#[test]
124+
fn test_window_title() {
125+
let mut app = create_test_app();
126+
app.add_plugins(game_plugin);
127+
128+
app.update();
129+
130+
let window = app.world_mut().query::<&Window>().single(app.world());
131+
assert_eq!(window.title, "This is window 0!");
132+
}

0 commit comments

Comments
 (0)