IN THIS LEVEL
A little work now will save a lot of work later.
Unit testing is something all developers should do, but many do not know how, do not want to, or feel they do not have time to do it. In this Level you will learn:
How to Install GDUnit4
How Unit Testing Can Save You Time
How to Write Tests
When To Write Tests (And When Not To)
Installing GDUnit4
We are going to start by downloading and installing the GDUnit4 Plugin for Godot.
Click AssetLib at the top center of the window. (Right of the Script button.) (The Go to the AssetLib View will open.)
Type “gdunit4” into the search bar.
Click on GdUnit4.
Click the Download button. (Wait for the download to complete.)
Click the Install button. (Wait for the install to complete.)
Click the Ok button.
Click the Ok button (again).
Click the Plugins… button. (Upper-right hand corner of the AssetLib window.)
Click the On checkbox next to gdUnit4.
Click the Close button.
Click the Update button. (If an update window appears.)
Click the Update button again. (If an update is available.) (Wait for the download to complete. This may take a while.)
Close and re-open your project. (If GDUnit4 does not automatically restart your project.
You now have the GDUnit4 plugin installed! You will have a new GDUnit tab in the upper-left hand corner dock after Scene and Import. The addons folder in FileSystem now has another folder where this plugin resides. Let’s get to testing!
“You may be wondering how we decided which unit testing library to use. It came down to integration. GDUnit4 is easier to integrate into a pipeline (i.e. GitHub) for automatic testing. Unit tests are only good if they run automatically.”
“Have you pushed your code to GitHub recently? If not, you should. We’ve added a lot. Don’t fret if Godot hangs for a moment when you’re staging your changes. Remember, it’s single-threaded.”
Writing Our First Test
Right now, the only thing we have to test, other than our import scripts, is our character.gd
script. We want to test every single function if we can. So let’s take a look at them:
First, there’s _physics_process()
. It has boilerplate code, and then delegates almost everything to other functions. There isn’t a lot to test here. Since it gets called every frame, and the only output is applying physics, it will get thoroughly tested by testing all its internal functions. This isn’t to say that we couldn’t test it, just that the value of testing it individually isn’t worth our time.
Second, is _unhandled_input()
. This is a built-in function like the first. This one handles mouse input and rotation. As long as the mouse is doing what we want, there’s no reason to test this. All we’d be doing is testing to make sure the Godot engine didn’t mess up mouse handling. Regressing Godot functionality it out of scope for us.
Next up, get_input()
. This is a one line function that calls another function we made. All it’s doing is creating a direction vector, and this is boilerplate Godot code. Testing Godot’s mapping seems out of scope right now as well. So we will move on.
Now we get to update_velocity()
. This is something we can sink our teeth into. An argument can be made that it’s boilerplate too, after all most of it was generated by Godot for us. But this allows us to test that our model is moving in the direction we expect when an outside script gives it a command, or it gets player input. This is useful for debugging.
Let’s say that our controls are suddenly not working. We can run this test. If it passes, we know that our bug isn’t with the movement code, but the get_input()
function - even though we didn’t write a test for that one. Now we know where to look. Likewise, if we are trying to move the character with a script and it’s not going where we want, if this test passes, we know the problem is with our other script. So let’s test it!
Open
character.gd
in the Script Editor.Right click on the
update_velocity()
function name.Select Create Test.
We now have a character_test.gd
file in our script editor, and it has been saved in test -> mobiles -> character_tests. How easy was that!? We have a stubbed out test named test_update_velocity()
. Let’s add some code.
Copy and paste the code to the right into your test_update_velocity() function.
Right-click the function name
test_update_velocity()
.Select Run Tests. (Bottom of the menu.) (Wait for the test to finish.)
Select the gdUnitConsole tab in the bottom dock.
You can see that our test ran, and it passed! Notice that the knight didn’t move though? This pass is what’s known as a false positive. The results say the test passed, even though we saw with our own eyes that it didn’t! What’s up?
Well first off, our knight didn’t move. Second, even if the model had moved, how would we know? So how do we fix these problems? The first, with code. The second, with a detector and an assertion in our test that makes sure our test passes.
The code is the first thing to tackle. We have two problems. The first is that update_velocity()
relies on the SpringArm3D’s rotation to modify the movement. We don’t want the SpringArm3D involved in our test. Second, even if we fix that, get_input()
is going to get called, get no input from the player, and run update_velocity()
again. So our command never gets to the physics engine. We need a way to tell the character class when it should listen to player input, and when it should ignore it.
We are going to do this with a bool(ean) value called is_player
. A bool is an on/off switch. It is either true or false. We are going to export this value so that we can easily turn it on and off in the editor, and we are going to set it to false by default. There should only ever be one player in our game, and so we will use this value to track that. Then we can turn off get_input()
if this value is false. Likewise, we can change update_velocity()
to detect the value, and change the first line based on whether or not it is player controlled.
Add this line at the top of your
character.gd
script right below the extends line:@export var is_player = false
Modify the
get_input()
call in_physics_process()
to read:if is_player: get_input()
Replace the first line in the
update_velocity()
method with:Press Ctrl + F5.
Our knight now walks backwards! However we still can’t assert that he’s moving with code. So, we are going to take a break from writing our test, and make a detector!
“Knowing what to test is important. Some things are worth your time, and some are not. You want to make sure you have coverage of the most important things, and the things that don’t get used very often. Other things will get tested by being utilized by other tests.”
if is_player: direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).rotated(Vector3.UP, spring_arm.rotation.y) else: direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
Making a Door
Remember how when using the GridMap, the door of our wall_doorway.gltf
wasn’t showing up? We are going to create a door object. Then we are going to put a detector on it so it can sense when a player approaches. Later, we will use this detector to have the door open up - and then close when the player walks away.
Select Scene -> New Inherited Scene.
Open
wall_doorway.gltf
. (assets -> models -> dungeon -> walls)Change the root node name to “
door
”.Save it in environment -> doors.
Click the Add Child Node button. (Plus (+) sign directly above the node name.)
Create an AnimationPlayer. (It will be a child of the root node if you had it selected; if not, drag and drop it onto the root node so it becomes a child of it.)
Add an AudioStreamPlayer3D node (as a child of the root node.)
Add an Area3D node (as a child of the root node.)
Rename the Area3D node “
MobileDetector
”.Turn off the Layer 1 collision layer for the MobileDetector. (Inspector -> CollisionObject3D -> Collision)
Turn off the Layer 1 collision mask.
Turn on the Layers 5, 6 and 7 collision mask.
Add a CollisionShape3D node as a child of MobileDetector.
Give it a CylinderShape3D shape. (Inspector -> CollisionShape3D -> Shape -> New CylinderShape3D)
Click the CylinderShape3D you just made to open its properties.
Change its Height to 2.5 m.
Change its Radius to 3 m.
Change the CollisionShape3D node’s y-position to 1.25 m. (Inspector -> Node3D -> Transform -> Position)
Now that we have a door, we are going to add it to our room in the next step.
“When necessary, we can create things in our games just for testing, but when possible, we want to utilize things that already have a purpose for our testing.”
“We are just going to assume at this point that you’ll remember to save and save often.”
Creating a Test Room
When we are unit testing, it’s good to be testing in an environment that we can control. So, we are going to create a test room. We also need to make sure our test room has a door, and for that we need a hold for the door.
Open
room_01.tscn
. (environment -> rooms)Delete a wall directly behind where your knight was standing in the main scene. (So that your knight will run into that space when he walks backwards in our test.)
Place a doorway there instead.
Save the file. (Ctrl + S)
Right-click on
main.tscn
. (Filesystem root.)Select Move/Duplicate To…
Select the test directory.
Click the Copy button.
Rename the copied scene to “
test_room.tscn
”. (Right-click -> Rename)Drag and drop
door.tscn
from FileSystem onto the Room01 node in the Scene panel.Press Y to turn snap mode on.
Click and drag the green rotation gizmo to turn the door.
Click and drag the green square to move the door into position in the gap. (See the screenshot to the right.)
Now we want to fix one little thing. When we added the is_player
variable to our knight, we defaulted it to off. You may have noticed that he moved when we tested in the main scene. So we want to go back to the main scene and turn is_player
back on so we can control him when we play the game.
Open
main.tscn
.Select Knight in the scene tree.
Turn Is Player on. (Inspector -> character.gd)
Now it’s time to finish our unit test!
“If you don’t have experience moving things around in 3D in Godot, check out the Introduction to 3D in the Godot Manual.”
Read More: Godot Tutorial: Introduction to 3D
Finishing Our First Test
Let’s go back to character_test.gd
. Notice how our test_update_velocity() test has three section. Every unit test should have these three sections: Setup, Execution, Assertion. Each section should be separated by a blank line.
Right now, our assertion section contains an await statement. This is a placeholder that we are using to keep the window open so we can see what is going on while we construct the test. We are now going to replace it with an await/assert combination that will look for a particular condition to be met. If it is within the time, the test passes. If it is not met within the time window, it fails. This is the preferred kind of await for testing, because as soon as the test passes, we don’t wait the rest of the time. So let’s update our test.
Change the first line to:
var runner = scene_runner("res://test/test_room.tscn")
(We can get this path by right-clicking on a file and selecting Get Path.)Add the following line to our Setup section:
var zone = runner.invoke("find_child", "MobileDetector")
Replace our await statement with an assertion (all one line):
await assert_signal(zone).wait_until(5000).is_emitted("body_entered", [character])
Re-run the test. (Ctrl + F5)
Did you see the window pop up very quickly and then pass? If you go to the gdUnitConsole tab, you can see the execution took less than 2 seconds.
If you got a failure, your knight isn’t running into the door. Comment out the assertion (Ctrl +K) and put back the await statement. ( await await_millis(5000)
) Run it again and see where your knight is going. Move your door, or update the vector you are passing to get him to walk towards it. Play with combinations of 1, 0, and -1 in the vector2 to send him in different directions. To get an angle other than 45 degrees, weight the two values as a percentages, like (0.75, 0.25).
Now that we’ve got one working test, let’s make some more!
“A unit test should never have more than one assertion. Otherwise, when a test fails, you won’t know why without digging.”
“We want our unit tests to run in fractions of a second if they can. When animations are involved, they will sometimes take seconds. Those seconds add up. NEVER use await_millis() in a final test. ONLY use it to see what’s going on while you construct a unit test. This is one of the key things that makes you stand out as a novice or experienced developer when it comes to unit testing.”
Testing Our Animation
One of the benefits of a unit testing framework like gdUnit4 is it has test hooks. These hooks are optional functions we can call to do things before or after all the tests in the file, or every test in the file. They are called before and after hooks.
The ones just named before()
and after()
run once each per file. The before()
hook runs at the beginning and is used to setup common variables. The after()
hook is used to put everything back the way we found it and clean up (for things that don’t cleanup after themselves.)
The before_test()
and after_test()
hooks run before every single test runs, and after every single test finishes. They are used to setup things between each test in your file. Typically you use one or the other.
We will be using the before() and after_test() hooks to setup our common variables at the beginning, and then reset the knight’s position after every test so we can move him again. Otherwise, the first test run will pass, and all others will fail.
So let’s set some things up:
Add three global variables above
test_update_velocity()
:var timeout = 5000
var runner: Variant
var character: CharacterBody3D
Add our Before hook (below that):
func before():
runner = scene_runner("res://test/test_room.tscn")
character = runner.invoke("find_child", "Knight")Add our After Test hook (after the Before hook):
func after_test():
character.global_transform.origin = Vector3.ZERO
character.global_rotation = Vector3.ZERODelete the first two lines in
test_update_velocity()
. (Therunner
andcharacter
variable declarations.)Change the
wait_until()
in the assert to use the newtimeout
variable (all one line):await assert_signal(zone).wait_until(timeout).is_emitted("body_entered", [character])
Run the test again.
The test will still pass if everything is entered correctly. If you watch closely you can see our knight pop back to the center of the map before the window closes. Now we are ready to test our animation.
Copy and paste the test to the right after
test_update_velocity()
.Run it. (Right-click on the name and select Run Test.)
That’s it! It takes somewhere around 150 milliseconds to run. (100 ms is 1/10th of a second.) You may not even see the screen pop up it’s so fast. With this test, we didn’t change the execution step at all. Instead, we changed the test by changing what we were assert - that the IWR animation was playing.
Next up, we are going to write four tests for rotate_to(), but for three of them, we need to update the function first.
“The Godot style guide says there should be two blank lines between each function.”
Read More: GDScript Style Guide
“It is important to know that just because you have unit tests in a certain order in your file doesn’t mean they will be executed in that order. Unit tests are meant to be atomic - which means they do not rely on each other to run. This rule is so sacrosanct, that many unit testing frameworks (like JUnit which GDUnit4 is based on) randomly play your tests in a different order every time you run them so that your tests must be atomic. Before and After hooks help us to be atomic.”
Read More: Why Automated Tests Should Be Atomic
Read More: Atomic Unit Testing
Testing Character Rotation
We can add our first rotate_to()
test right now, so we will. We are going to name it test_rotate_to_spring_arm()
though, to differentiate it with the three other rotate_to()
tests we are about to add.
Copy and paste the test to the right at the bottom of our file.
Run the test.
There’s a few things to point out about this test. First, we are resetting the SpringArm3D back to where it was to begin with inside the test function when we are done with it. This is because none of the other tests need it for their testing. So there’s no point in making a class variable and resetting this value every time. It’s just wasted processing power and time.
Second, our is_equal_approx()
statement. This is specifically for vector testing, and allows for us to look for a vector in the approximate area we expect, with a level of acceptable deviation. This is because we don’t always have control over where exactly something is going to point. Close enough in this case, is a pass.
Finally, we set a Vector3(0, 1, 0) and we were looking for a Vector3(0,0.2,0). Why? Because we were dividing it by delta in the function. We gave ourselves a 0.1 margin for error because delta can change. As long as it’s not a Vector3(0,0,0) we know the rotation happened. (You may need to up this to 0.2 if the test is’t passing.)
Writing Unit Tests First
Now let’s say we want to rotate towards something the SpringArm3D (and the camera) isn’t pointing at? How would we do that? It turns out that rotation in 3D space is one of the hardest problems to solve. It can result in the camera (or model) turning on weird axes you don’t intend. The solution for that is to use a Node3D function called look_at(). This allows us to look at a Vector3 coordinate and also indicate which way is up, so the engine knows how to rotate to prevent disorientation.
While being able to rotate to a Vector3 is a start, what if we only have a Vector2? We should solve that problem too. Also, it would be really nice to be able to rotate towards any node in a scene. This would be super useful later when we want enemies to eyeball the player, or NPC shopkeepers to look at the player when they want to buy something. So we should add in rotating towards objects.
We now have four cases we want to write code for:
SpringArm3D (Done!)
Vector3
Vector2
Node
So, let’s write three more tests. Then when we’ve added in the code to make these work, we will know they’re working! What can we rotate towards? Well, we have a door…
Copy and paste the three tests from the right into your
character_test.gd
file.Right-click in the file, but not on any test name. (We want to run all the tests.)
Select Run Tests.
All our tests fail! What’s going on? Our rotation function is being called when it shouldn’t - i.e. when we aren’t a player. Remember how we had an inline if statement in the physics processor for rotate_to()
? This is why. We can just add another little test, and this rotation only gets called when the character is player controlled:
add
and is_player
to character.gd like so:if velocity.length() > 1.0 and is_player: rotate_to(delta)
Re-run the tests.
That’s better. Now we have what we expect. 3 failing tests. Two are failing because our rotate_to()
function doesn’t know how to handle vectors yet. The Node is giving us a weird error though. It’s rotating, but not towards the door. That’s all ok. We expect them to fail - we haven’t added the code to deal with them yet! Let’s take a look at these tests and see what they’re doing.
Parameterization
Our first test is finding the door. Then we are using the x and z positions to create a Vector2 and passing that to the rotate_to()
function. Our second test is finding the door as well. Then we’re using the global_position class variable to get its position and just pass that in. Our third test just passes the door straight up.
All three tests are identical except for the execution. That seems like a waste of typing. It turns out that GDUnit4 has something called parameterization. This allows us to pass a list of variables that are different when a test is almost identical, and reduce it to one test that runs multiple times.
First, we need access to the door when we create the test, so it needs to become a class variable that is assigned before this test runs.
Add
door
to the class variables list at the top of the file:var door: Node3D
Assign a value to
door
in the Before hook:door = runner.invoke("find_child", "door")
Copy the parameterized version of the method to the right.
Delete the three individual tests we made.
Run the tests again.
Ok, we still have three failures, but they’re grouped together now! The last thing to do is implement the functionality to make these tests pass.
“Unit Testing is typically part of Test-Driven Development (TDD). That means you write your tests BEFORE you write your code. It seems backwards, but it has a number of benefits. The primary one is it is a guard against procrastination. No one ever wants to write tests after they’re done coding. So, if we front-load the effort, it is more likely to get done. Typically, you want 60-80% test coverage in a well-tested product.
TDD was made popular by Extreme Programming (XP), which is one of the first implementations of Agile Software Development. XP’s goal was to rapidly prototype and develop code that worked and was maintainable. The focus is on testing the things that can possibly break. (Many business people think Agile means the Scrum method, but that’s like saying a Rectangle is always a Square.)”
Read More: A History of Unit Testing (Including Extreme Programming)
Read More: The Agile Manifesto (Where the term “Agile” originated)
Implementing New Ways to Rotate
Let’s replace our rotate_to()
function and then discuss it.
Copy and paste the function to the right into your
character.gd
file and overwrite the previous implementation.Update the call to
rotate_to()
in_physics_process()
:if velocity.length() > 1.0 and is_player: rotate_to(spring_arm, delta)
Run the tests again.
All six test are passing now! Our new function take a Variant target variable, and a delta float. To make this function easier to use, we assume a delta value equal to what it would be for 60 frames per second. This is because unless the target is a SpringArm3D, we don’t use this value. We then look at the target and do different things with it depending on its type.
If it’s a SpringArm3D, we run our original code and then return so the rest of the code in the function doesn’t get run.
Then only if it’s not a SpringArm3D, we check to see if it’s a Vector2. If it is, we add the character’s global_position.y value, and move the existing y value to the z value. (In Godot’s 3D space y is the up and down axis.) Then we assign this to target, and it’s as if the function was originally passed a Vector3. We don’t return at this point, and we fall through past the next elif (else-if) to the last statement.
If the target wasn’t a SpringArm3D or Vector2, but is a Node, we grab it’s global_position and make target equal to that Vector3 value. Then on to the next line!
The last statement now runs, and we’ve converted all three supported argument types into a Vector3. We use the Node3D look_at()
function and we are done!
Now that we’ve got rotation working, we’re ready to use it to make something more complicated. We can use it as part of a navigation system. After all, if it’s nice to be able to have a character look at something, it’d be even better to have them walk over!
Level Up!
Congratulations! You’ve made it through a very technical level! You’ve now started to learn something that even some experienced developers don’t know how to do! You should be proud of yourself! This is a major accomplishment!
Unit testing is going to be something we keep up as we move forward. Next up we are going to use it while we add in navigation for our character. We will use that to test our door - after we animate it to open and close, add some sound to it, and give it an optional key!
“Level 6! You’re moving right along! Unit testing is a tool that will save you tons of time in the future by telling you as soon as you introduce a bug, and where it is! The smaller your development team, the more important this is!
If you want to check your work, or you were having problems getting things going, we have links to the full files below.”
Get the Code: character.gd
Get the Code: character_test.gd