gdtest

Assertions Reference

Complete API reference for all gdtest assertion functions.

All functions below are available as unqualified calls when your test class extends GdTest, or as Gdt.* static calls when it must extend an engine type. Functions written with a Gdt. prefix exist only as static calls.

For a walkthrough with context, see Writing Your First Test.


Equality

assert_eq(actual, expected, msg = "")
assert_ne(actual, not_expected, msg = "")
assert_lt(actual, bound, msg = "")
assert_lte(actual, bound, msg = "")
assert_gt(actual, bound, msg = "")
assert_gte(actual, bound, msg = "")
assert_eq(calc_damage(10, 3), 7)
assert_gt(player.hp, 0)
assert_lte(elapsed_ms, 100)

Float & approximate

assert_almost_eq(actual, expected, tolerance = 1e-6, msg = "")
assert_approx(actual, expected, tolerance = 1e-6, msg = "")   # alias
assert_almost_eq(lerp(0.0, 1.0, 0.5), 0.5)
assert_approx(sin(PI), 0.0, 1e-5)

Boolean & null

assert_true(cond, msg = "")
assert_false(cond, msg = "")
assert_null(value, msg = "")
assert_not_null(value, msg = "")
assert_true(player.is_alive())
assert_null(inventory.find_item("nonexistent"))

Collections

assert_has(collection, value, msg = "")
assert_not_has(collection, value, msg = "")
assert_is(obj, SomeClass, msg = "")
assert_between(value, low, high, msg = "")

assert_has works on Array, Dictionary (checks keys), and String (substring check).

assert_has(inventory.items, "sword")
assert_is(spawned_node, Enemy)
assert_between(player.level, 1, 18)

Strings

assert_string_contains(haystack, needle, msg = "")
assert_string_starts_with(s, prefix, msg = "")
assert_string_ends_with(s, suffix, msg = "")
assert_string_contains(error_message, "out of bounds")
assert_string_starts_with(save_path, "user://")

Spatial types

All use distance-based comparison:

assert_vec2_approx(a, b, tolerance = 1e-5, msg = "")
assert_vec3_approx(a, b, tolerance = 1e-5, msg = "")
assert_vec4_approx(a, b, tolerance = 1e-5, msg = "")
assert_color_approx(a, b, tolerance = 1e-5, msg = "")   # max channel diff
assert_rect2_approx(a, b, tolerance = 1e-5, msg = "")   # pos + size
assert_vec2_approx(projectile.velocity.normalized(), Vector2.RIGHT)
assert_color_approx(tween_result, Color.RED, 0.01)

Signals

All signal assertions must be awaited:

await assert_signal(node, "signal_name", timeout_ms = 1000)
await assert_no_signal(node, "signal_name", timeout_ms = 200)
await assert_signal_count(node, "signal_name", expected_count, timeout_ms = 1000)
await assert_signal_with_args(node, "signal_name", [arg1, arg2], timeout_ms = 1000)
func test_player_emits_died() -> void:
    player.hp = 0
    player.take_damage(1)
    await assert_signal(player, "died")

assert_no_signal passes if the signal is not emitted within the timeout.

Watching and ordering signals

var watcher = watch_signal(obj, "signal_name")   # records every emission
watcher.count          # number of emissions so far
watcher.last_args      # arguments of the latest emission
watcher.emissions      # every emission's arguments
assert_emitted_before(obj_a, "sig_a", obj_b, "sig_b", msg = "")
assert_signal_order([{obj = a, sig = "started"}, {obj = b, sig = "finished"}], msg = "")

Order assertions compare the first emission of each signal, which must be watched with watch_signal before it fires. Watchers disconnect after each test.


Snapshots

assert_snapshot(actual, name = "")

Serializes actual with var_to_str and compares against a stored .gdsnap file. First run creates the file. See Snapshot Testing.


Control flow

fail(msg = "")     # unconditional failure
pending(msg = "")  # marks test pending, continues execution
skip(msg = "")     # marks test skipped, continues execution

Soft assertions

Gdt.expect(cond, msg = "")
Gdt.expect_eq(actual, expected, msg = "")    # also expect_ne, expect_lt, expect_lte,
Gdt.expect_true(cond, msg = "")              # expect_gt, expect_gte, expect_approx,
Gdt.expect_null(value, msg = "")             # expect_false, expect_not_null
assert_all(checks: Callable) -> bool

Every assertion records its failure and lets the test continue, so one run reports every broken check. assert_all runs a block of checks and returns false if any failed, to stop a test whose setup is broken:

if not assert_all(func() -> void:
    assert_eq(player.hp, 100)
    assert_eq(player.mana, 50)
):
    return

Async helpers

await wait_physics_frames(n = 1)
await wait_physics_seconds(seconds)
await wait_process_frame()
await wait_until(predicate, timeout_ms = 2000)   # until predicate returns true
await wait_while(predicate, timeout_ms = 2000)   # while predicate returns true
set_physics_delta(seconds)   # fixed physics step for this test
reset_physics_delta()

Scene helpers

add_child_autofree(node) -> Node      # adds node as child; freed after test
instantiate_scene(packed) -> Node     # instantiates and adds as child; freed after test
scene_runner(packed) -> SceneRunner   # see SceneRunner API Reference
autoref(obj) -> Object                # frees or releases obj after the test

test_world() gives a scene container that is cleaned up after the test:

var world = test_world()
var actor = world.spawn(Enemy)          # class must extend Node
await world.physics_frames(10)          # also process_frames(n), ready()

Lifecycle hooks

func before_all() -> void    # once before any test in the file
func after_all() -> void     # once after all tests in the file
func before_each() -> void   # before every test_*
func after_each() -> void    # after every test_* (runs even on failure)

Inline skip & focus

PrefixBehaviour
xtest_Always skipped
ftest_Only this function runs in the file
func xtest_not_implemented_yet() -> void: pass
func ftest_the_one_i_am_debugging() -> void: ...

Parameterized tests

Define a get_test_<name>_cases() function that returns an Array of [label, [args...]]:

func get_test_damage_cases() -> Array:
    return [
        ["zero defense", [10, 0, 10]],
        ["partial defense", [10, 3, 7]],
        ["full block", [5, 10, 0]],
    ]

func test_damage(attack: int, defense: int, expected: int) -> void:
    assert_eq(Battle.calc_damage(attack, defense), expected)

Each case emits a separate result: test_damage[zero defense], test_damage[partial defense], etc.


Describe blocks

Inner classes named describe_* are treated as sub-suites. Each must declare extends GdTest:

class describe_Battle extends GdTest:
    func test_basic_hit() -> void:
        assert_eq(Battle.calc_damage(10, 3), 7)

    func test_crit() -> void:
        assert_eq(Battle.calc_damage(10, 3, true), 14)

Reported as describe_Battle/test_basic_hit, describe_Battle/test_crit.


Property tests

Gdt.forall(generators: Array, check: Callable, runs = 100) -> bool
Gdt.gen_int(min, max)
Gdt.gen_float(min, max)
Gdt.gen_string(max_len = 10)
Gdt.gen_bool()
Gdt.gen_choice([a, b, c])

forall calls check with one generated value per generator. On failure it shrinks the inputs to a smaller failing case and prints it with the seed; gdtest --seed <seed> replays it. See Running Tests.


Performance

Gdt.bench(callable, iterations = 100) -> BenchResult   # mean_ms, median_ms, p95_ms, p99_ms, min_ms, max_ms
Gdt.assert_completes_in(result_or_callable, max_ms)     # median under max_ms
Gdt.assert_faster_than(result_a, result_b, factor = 1.0) # a at least `factor` times faster than b

Errors and warnings

Gdt.push_error(msg)       # use in game code instead of push_error() to make it assertable
Gdt.push_warning(msg)
assert_push_error(text, msg = "")     # text, or a regex written as "/…/"
assert_push_warning(text, msg = "")
assert_no_push_error(msg = "")

Memory

assert_no_new_orphans(msg = "")   # no orphan nodes since before_each
current_heap_delta() -> int       # static memory change in bytes since the test started

Properties and exports

assert_exports(obj, "speed", TYPE_FLOAT, msg = "")
assert_property(obj, "speed", 300.0, msg = "")            # default value on a fresh instance
assert_readonly_property(obj, "id", msg = "")             # setting it has no effect
assert_property_with_range(obj, "volume", 0.0, 1.0, msg = "")   # @export_range bounds

Files

assert_file(path).exists()                  # also is_file(), is_directory(), is_empty()
create_temp_dir(name) -> String             # absolute path, created fresh
delete_temp_dir(name)

Fluent assertions

Gdt.assert_that(value).is_not_null().is_equal_to(42)   # any value
Gdt.assert_int(value).is_between(1, 10)
Gdt.assert_float(value).is_equal_approx(0.5, 0.01)
Gdt.assert_str(text).starts_with("user://").ends_with(".save")
Gdt.assert_array(items).contains_exactly_in_any_order(["a", "b"])
Gdt.assert_dict(data).contains_key_value("hp", 100)
Gdt.assert_object(node).is_instanceof(Enemy)
Gdt.assert_bool(flag).is_true()
await Gdt.assert_signal_of(node).is_emitted("died")

Every fluent check returns the assertion, so checks chain. These mirror gdUnit4's API, see Migrating from gdUnit4.