Photo by ThisisEngineering on Unsplash

Unit testing GDScript with GUT

Who says you can’t unit test your game?

Stephan Bester
5 min readJun 19, 2024

--

I’ve been tinkering with Godot for a while and I’m starting to get more comfortable with GDScript. I recently found myself writing and rewriting some logic that’s quite decoupled from the game engine and thought, “It would be really cool to write some automated tests for this.”

I discovered the Godot Unit Testing (GUT) project and decided to give it a shot. So far it’s proven easy to use and it has allowed me to bring the power of unit testing to my game dev hobby (without having to write my own framework for it). Below is a quick guide I wrote for myself as I first set it up.

How to set up GUT

Installing GUT

Detailed instructions for installing GUT can be found here. GUT is installed on a per-project basis via the Godot asset library. Start by clicking the AssetLib button.

The AssetLib button

In the asset library, search for and install the asset named “GUT - Godot Unit Testing (Godot 4)”.

GUT - Godot Unit Testing

By default, this installs GUT in the “addons” directory of your current project.

GUT installed in addons

Once installed, GUT needs to be activated. You can do this on the “Plugins” tab of the “Project Settings” dialogue.

Enabling GUT in the Project Settings

Tick the “Enable” checkbox for the plugin named “Gut”. A new panel should appear at the bottom of the editor.

The GUT editor panel

Checking that it works

If you click the “Run All” button immediately, you get an error message like the following:

Cannot run tests, you have a configuration error:
* You do not have any directories set.
Check your settings ----->

Create one or more directories for saving your tests. Then, in the GUT panel, scroll down to the “Test Directories” section. Here, enter each of the directories you want GUT to load tests from, e.g. if you have one directory named test then simply enter “test” in the 0th row (the toggle switch flips automatically).

Capturing Test Directories in GUT

You can test that your configuration works by clicking the “Run All” button again, which should greet you with a report such as the following.

Result of “Run All” with no tests

Of course, this is not very useful until we’ve written some tests.

How to write tests

Test scripts naming

By default, test scripts are expected to begin with the prefix test_ , e.g. test_foo.gd. You can change this along with the expected suffix under the “Misc” section in the GUT settings.

Script Prefix and Suffix in the GUT

For example, if you want to name your tests in the style FooTests.gd instead of the default test_foo.gd, simply remove the prefix and change the suffix from .gd to Tests.gd.

Test script conventions

All test scripts need to extend the base type GutTest .

Furthermore, each test method name has to begin with the prefix test_, e.g. test_method_works(). Unlike the file name prefix, this does not appear to be configurable. Test methods also do not take any parameters (unless you’re writing a parameterized test, in which case it has a specific signature).

For example, given the following script:

class_name Foo

func bar(number: int) -> String:
match(number):
1:
return "Baz"
2:
return "Qux"
return ""

A test script might look like this:

extends GutTest

func test_foo_bar_1_returns_baz():
var sut = Foo.new()
assert_eq(sut.bar(1), "Baz")

func test_foo_bar_2_returns_qux():
var sut = Foo.new()
assert_eq(sut.bar(2), "Qux")

func test_foo_bar_other_returns_blank():
var sut = Foo.new()
assert_eq(sut.bar(0), "")

Running all tests gives the following feedback:

Test results for test_foo.gd

If you declare inner classes that start with the prefix Test e.g. TestBar then GUT will use those to group test methods in the results. For example, let’s say our earlier Foo class has another method Hello which needs testing. We could structure our test script this way:

extends GutTest

class TestBar:
extends GutTest

func test_foo_bar_1_returns_baz():
var sut = Foo.new()
assert_eq(sut.bar(1), "Baz")

func test_foo_bar_2_returns_qux():
var sut = Foo.new()
assert_eq(sut.bar(2), "Qux")

func test_foo_bar_other_returns_blank():
var sut = Foo.new()
assert_eq(sut.bar(0), "")

class TestHello:
extends GutTest

func test_hello_world():
var sut = Foo.new()
assert_eq(sut.hello("World"), "Hello World")

Now, the test methods for each inner class appear under a different heading in the test results, e.g.

Test results using inner classes

Next steps

GUT has some good documentation. There are a lot of assertion methods and it’s worth familiarizing yourself with them. The library also supports doubles, spies, etc. for more advanced scenarios.

At the moment, I’m more focused on testing logic that’s isolated from the main game loop, but I’ve noted that GUT has support for simulating calls to engine methods like _physics_process. If I decide to extend automated testing to my game objects, I’ll be sure to look into these.

Used for this article

  • Godot Engine v4.2.1
  • GUT - Godot Unit Testing (Godot 4) v9.2.1
  • Windows 10 Pro

--

--

Stephan Bester

Software developer walking the edge between legacy systems and modern technology. I also make music: stephanbmusic.com