← All posts

gdextest is public: test your GDExtensions like you mean it

gdextesttestingrelease


Today’s short announcement: gdextest — the GDExtension Test Framework — is now public. Repository, full documentation, the works. Start here: Getting started.

The problem it solves

Testing a GDExtension used to mean one of two things. Either you built an entire test game — scenes as test cases, a script that prints PASS/FAIL into the output panel, you squinting at a wall of text — or you poked at your code through a debugger and called it testing. Neither scales, neither runs in CI, and neither tells you anything when you come back tomorrow.

gdextest flips it: you write googletest-style tests in C++, and they run inside a real, headless Godot process. Not mocks, not stubs — the actual engine, booting your code.

How it works

The framework and your suites compile together into one test-only shared object. A generated fixture project loads it, Godot 4.5 runs headless, the summary prints, the process exits 0 or 1. One command does the whole dance:

git submodule add <repo-url> extern/gdextest
git submodule update --init
// tests/smoke.cpp
#include "gdextest/assert.h"
#include "gdextest/registry.h"

GDEX_TEST(smoke, framework_is_wired) {
    GDEX_EXPECT(true);
}
./extern/gdextest/gdextest test

That’s a green run. No scene plumbing, no test game, no eyeballing.

What you get

  • Familiar assertions. GDEX_EXPECT_EQ, GDEX_ASSERT_NOT_NULL, GDEX_EXPECT_STR_CONTAINS, GDEX_EXPECT_THROW — expectations record and continue, assertions stop the test. If you’ve written C++ tests before, there is nothing to learn.
  • Real engine access. Pure-logic tests need no engine. Tag a test TAG_INTEGRATION and you reach singletons, ClassDB and the live scene tree — create real nodes, parent them, free them.
  • Async tests. C++20 coroutines that suspend across engine frames: co_await ctx.await_frames(2). Tweens settle, timers fire, deferred calls land — and every wait has a timeout, so a stuck test fails instead of hanging CI.
  • Signal monitoring. ctx.signals() watches any object’s signals, counts emissions and records the arguments of every emission. Assert that the signal fired, fired once, and fired with the right payload.
  • Leak detection built in. ctx.track_object(node) and ctx.track_ref(ref) fail the test if the object is still alive at teardown. LIFO teardowns run even when the test aborts or skips.
  • Zero release footprint. The framework only compiles into the test build. Your shipped extension never sees it.

CI from minute one

gdextest init --ci writes a GitHub Actions workflow for you. Output comes as JSON or JUnit XML, runs can be sharded across machines and merged back with gdextest report — which itself exits 1 on any failure, so the merge step is also a gate. The doctor preflight checks config, Godot, SCons and godot-cpp before anything runs, so a broken environment fails loudly instead of mysteriously.

Where it came from

gdextest wasn’t built to be announced — it was built because Godot Code Studio needed a way to test itself, and the test-game approach made that miserable. The framework spent months quietly hardening against the studio’s own projects before it earned a public repo.

If you ship GDExtensions, give it a weekend. The full documentation covers everything from your first suite to sharded CI — and if something fights you, that’s what the issue tracker is for.

Back to the GCS trenches next.

— 5cump1 🦐