Writing clean and maintainable code often follows well-defined practices for structuring and testing software. In today’s example, we’re going to go cover testing and test-driven development, specifically in Godot. While the tools mentioned may be unique to Godot, the principles here can be applied to any non-trivial software project. After some deliberating, we’ve drilled down our development and testing pipeline to the following.
Clear separations of scenes in Godot is key to ensuring your project can be unit tested. There are also numerous other benefits to keeping your project well-organized, modular, and atomic. But for now, let’s stay within the scope of testing. When scenes are kept atomic, they can be built within a smaller scope. A scope that is, hopefully, easier to test against when compared to the project as a whole. Atomic components can be tested with the knowledge that they maintain a set of valid inputs and outputs. It’s tricky (but not impossible) to test a component without pre-defined I/O but keeping scenes modular can help reduce the overall number of “tricky” tests needed. Modular scenes can have their components tested individually without having to worry about secondary interactions between other components. Our tests could furthermore serve as documentation; expected states in tests can serve as a sort of API interface for your components. You should also write documentation, of course, but having working examples of test cases on a given scene can facilitate future development. The crux here is that if your one component takes a set of inputs X and returns a set of outputs Y, then your confidence of the entire system increases. If a scene passes its unit tests, then you can run with the assumption that it will work when other components are supplying the inputs.
Every scene needs tests. This serves two purposes: it forces developers to truly understand the results and consequences of their scene, and it gets developers in the habit of writing tests as they develop. Too often, new scenes are built without tests. These scenes are added to larger scenes and cause problems. Once a scene is being used by another, it becomes much harder to nail down where things went wrong. Godot’s scene architecture makes this easier some other engines out there, but pernicious problems can still cause amok. Testing your scenes atomically before they’re added to a parent scene helps ensure predictability in your larger applications. It reduces the number of trivial issues in your scenes while exposing non-trivial issues during interactions. Signals are especially annoying; sending a signal from one scene and handling it elsewhere often requires a developer hooking up the signal to a function in your scene. These interactions are fairly loose (if you consider nested scenes to be “tight,” for example) and as a result, they should be tested thoroughly. Ensure that signals are being handled properly before hooking them up to your levels.
Finally, consider logging all important actions. Add a DEBUG flag and flip it when building from your development environment. When in DEBUG mode, log everything to
/var/log/GAMENAME.log or some other file. Printing works well enough but as a game grows in complexity, you may find it helpful to refer to previous logs. If you print your log messages to console, they may be overwritten the next time your project is built or executed. Logging helps developers and QA debug their backend quietly. A helpful rule we try to follow is that every interaction should be logged. Period. This mimics DB logging; every database interaction is usually logged (maybe using ARIES) so that upon a crash, it can have its transactions redone or undone. Similarly, anytime your project interacts with another component, log it. When something goes wrong between two scenes, your logs can help identify which components caused the issue and why. While game development isn’t as complex as say, kernel development, a lot of the same concessions and tips made building kernels are helpful for non-trivial game development. Single-report debugging is impressive and all, but tracing complex interactions manually after-the-fact is a lot more realistic. Spend time ensuring logging is present to assure yourselves that problems you missed in unit testing can be caught. If you’re properly unit testing, the issues you’ll be debugging will be interaction-based problems. What goes wrong when two scenes talk to one another. Use logging in tandem with unit testing to round out your QA pipeline.
The following tools and frameworks can help you structure and write excellent tests:
We did not go over specifics in this article because the GUT and WAT docs are quite extensive. Please refer to them for specific examples and quick-start guides.