For a change of pace, I'd like to do a bit of a dev log. Some time ago, I participated in a game jam and made this game – Of Mice and Bad Choices – a short puzzle game, where you place cheese around the maze to lure the mice out. It was fun, but there were evidently some shortcomings.
One of the major ones is the behavior of mice is unintuitive. The players mentioned that they would expect mice to be repelled by a disliked cheese, not just freeze. Plus, implementing this mechanic would allow for a much more rich puzzle design.
So, I think this is a good opportunity how automated testing can be done in Godot.
There are a few testing frameworks that are available for Godot 4, but the one that caught my I is Godot Unit Test (GUT). GUT is pretty simple:
test_
and write some assertations – typical unit test structure.
For this particular case, I wanted to have a way of defining complex scenarios, the same way I define levels for the game – in engine editor rather than in code (this way, the tests would be closer to reality). Hence, I want to do these things:
So, let's unwrap this.
Let's define a new class `MouseTestCase.` We want it to inherit Node2D
(as we want to place it on a scene. And we want it to find to of its children (that we will place on a scene ourselves): a mouse and its expected final position (as a Marker)
extends Node2D
class_name MouseTestCase
@export var steps_left = 0 # How many steps to simulate
@export var done = false
@onready var mouse: Mouse = $Mouse
@onready var expected_position = SnapUtils.get_tile_map_position($TestMarker.position)
Now, we can put it on a scene, and we are good! We know where a mouse starts, we know where it should end up, and in how many steps.
Now, let's make a bunch more of them, and make a map to test our repellent behavior.
This behavior is somewhat complex, hence, we want to cover many slightly different cases:
The resulting map defining 12 test cases to cover this behavior is shown above (imagine how tedious it could be to hard code all those coordinates in code).
The only thing left to do is the test runner function. The function needs to:
The code is quite simple.
func run_level_with_mouse_test_cases(map_path: String):
var level = load(map_path)
map.load_level(level)
var cases = MouseTestCase.cast_all_cases(get_tree().get_nodes_in_group(MouseTestCase.MTC_GROUP_NAME))
while (cases.any(func(case): return not case.done)):
map.move_mice()
for case in cases:
if not case.done:
case.steps_left -= 1
if case.steps_left == 0:
case.done = true
assert_eq(case.get_mouse_position(), case.expected_position, case.get_parent().name+"/"+case.name)
I imagine this will evolve, but the current implementation is good enough for now. I've written the tests, implemented the mechanic, and the tests actually confirm that the mechanic is implemented correctly!
Here, I've shown one way to approach the tests in games. Obviously, there are many more things to improve here, and I encourage readers to take the code and the framework and adapt it to their needs.
As always, the code is available on GitHub: https://github.com/d-lowl/of-mice-and-bad-choices You can also have a look at the specific PR that introduces testing. For bonus points, if someone can make them work in CI, that'd be brilliant. Cheers.