In GUT, queue input on a GutInputSender aimed at Input and await its idle signal before you assert; in GdUnit4, call the scene runner's simulate_key_*, simulate_action_* and simulate_mouse_button_* methods, wait, and await runner.await_input_processed().

Ziva.sh publishes this page and makes Ziva. We ran every test and command on this page on 7 November 2026 with Godot 4.7.2 on Linux, GUT 9.7.1 and GdUnit4 6.2.3, the latest releases that day, on the public Star Catcher sample, a 2D game where the arrow keys move a square into stars. Method names come from the installed addons' source code.

GUT

GUT's GutInputSender sends input events to receivers. With Input as the receiver, GUT 9.7.1 passes each event to Input.parse_input_event, so the game sees it like hardware input. Chained calls are queued and played back in real time, and await _sender.idle waits until the queue is empty.

# res://test/test_input.gd extends GutTest var _sender := GutInputSender.new(Input) func after_each() -> void: _sender.release_all() _sender.clear() func test_holding_right_arrow_collects_a_star() -> void: var main: Node = add_child_autofree(load("res://main.tscn").instantiate()) _sender.key_down(KEY_RIGHT).hold_for(1.2).wait(0.3) await _sender.idle assert_eq(main.get_node("HUD/ScoreLabel").text, "Score: 1") func test_holding_ui_right_action_collects_a_star() -> void: var main: Node = add_child_autofree(load("res://main.tscn").instantiate()) _sender.action_down("ui_right").hold_for(1.2).wait(0.3) await _sender.idle assert_eq(main.get_node("HUD/ScoreLabel").text, "Score: 1") func test_clicking_a_button_emits_pressed() -> void: var button: Button = add_child_autofree(Button.new()) button.position = Vector2(300, 200) button.size = Vector2(100, 40) watch_signals(button) _sender.mouse_left_click_at(Vector2(350, 220)) await _sender.idle assert_signal_emitted(button, "pressed") 

hold_for(1.2) sends the matching release 1.2 seconds after the press, and wait(0.3) gives the game time to react; both also take a frame count as a string such as "10f". GutInputSender also has key_up, action_up, mouse_left_button_down, mouse_left_button_up, mouse_right_button_down, mouse_right_button_up, mouse_double_click, mouse_motion, mouse_relative_motion, wait_frames and hold_frames. GUT's docs say to call release_all() "between each test when using Input as a receiver" and clear() "so that state does not leak between tests", which is what after_each does here.

Under xvfb-run with -gcompact_mode, all three tests passed. Headless, the two keyboard tests passed and the click test failed; see Pitfalls.

GdUnit4

GdUnit4's scene runner loads a scene, or wraps a node you pass it, and feeds simulated input through Input.parse_input_event. GdUnit4's docs say "you must wait at least one frame cycle after simulating inputs" and point to await runner.await_input_processed().

# res://test/test_input.gd extends GdUnitTestSuite func test_holding_right_arrow_collects_a_star() -> void: var runner := scene_runner("res://main.tscn") runner.simulate_key_press(KEY_RIGHT) await await_millis(1200) runner.simulate_key_release(KEY_RIGHT) await runner.await_input_processed() assert_str(runner.find_child("ScoreLabel").text).is_equal("Score: 1") func test_holding_ui_right_action_collects_a_star() -> void: var runner := scene_runner("res://main.tscn") runner.simulate_action_press("ui_right") await await_millis(1200) runner.simulate_action_release("ui_right") await runner.await_input_processed() assert_str(runner.find_child("ScoreLabel").text).is_equal("Score: 1") func test_clicking_a_button_emits_pressed() -> void: var button: Button = auto_free(Button.new()) button.position = Vector2(300, 200) button.size = Vector2(100, 40) var runner := scene_runner(button) monitor_signals(button, false) runner.set_mouse_position(Vector2(350, 220)) runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) await runner.await_input_processed() await assert_signal(button).is_emitted("pressed") 

GdUnit4 6.2.3 has three methods for each kind of input:

Input Press and release Press only Release
Key simulate_key_pressed(KEY_ENTER) simulate_key_press(KEY_SHIFT) simulate_key_release(KEY_SHIFT)
Mouse button simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) simulate_mouse_button_press(MOUSE_BUTTON_LEFT) simulate_mouse_button_release(MOUSE_BUTTON_LEFT)
Input action simulate_action_pressed("ui_accept") simulate_action_press("ui_right") simulate_action_release("ui_right")

GdUnit4 moves the mouse with set_mouse_position(position) and simulate_mouse_move(position), or gradually with simulate_mouse_move_relative and simulate_mouse_move_absolute. In GdUnit4 6.2.3, simulate_key_pressed waits one frame between the press and the release, so await it, while simulate_action_pressed and simulate_mouse_button_pressed press and release within the same call. For a key combination, GdUnit4's key docs say to hold each key with simulate_key_press, such as KEY_CTRL then KEY_C, because the shift and control arguments are deprecated and "will be removed in v7.0".

Under xvfb-run, GdUnit4 6.2.3 passed all three tests. GdUnit4's master branch, the unreleased 7.0.0 (commit 2cc420a), also passed all three with --headless.

Outside a test framework

Godot itself has two ways to fake input from code:

# A key event: _input() sees it, and the ui_right action mapped to that key becomes pressed. var key := InputEventKey.new() key.keycode = KEY_RIGHT key.pressed = true Input.parse_input_event(key) # An action only: Input.is_action_pressed("ui_right") is true, but _input() is never called. Input.action_press("ui_right") Input.action_release("ui_right") 

In our headless Godot 4.7.2 run, a node's _input printed the parsed key's press and release and nothing for action_press, and Input.is_action_pressed("ui_right") was true after both. Godot's Input docs say parse_input_event "Also generates Node._input() calls" and that action_press "will not cause any Node._input() calls". To release a parsed key, send another InputEventKey with pressed = false. Neither call touches the operating system: Godot's docs say sending Alt/Cmd + Tab as an InputEventKey "won't toggle between active windows".

Pitfalls

  1. Headless runs don't click Controls. With godot --headless, Godot 4.7.2 delivered our simulated clicks to _input but never pressed a Button: the click test failed in GUT 9.7.1, in GdUnit4 6.2.3 and in a plain project using Input.parse_input_event, and passed in all three under xvfb-run (GUT with -gcompact_mode). GdUnit4 6.2.3's own warning says "Godot 'InputEvents' are not transported by the Godot engine in headless mode", although our key and action tests passed headless. GdUnit4's unreleased master runs the scene in its own SubViewport, and there the click test passed headless.
  2. GUT's results panel covers the game. Under xvfb-run, GUT 9.7.1's click test still failed, because GUT's output panel, on a CanvasLayer at layer 128, was the control under the click. With -gcompact_mode the click reached the button and the test passed.
  3. GdUnit4 6.2.3 refuses --headless. GdUnit4 6.2.3's runtest.sh --headless stops with "Headless mode is not supported!" and exit code 103 before running any test, unless you add --ignoreHeadlessMode. GdUnit4's master says headless "is supported by default".
  4. simulate_frames counts frames, not seconds. GdUnit4's simulate_frames(72) waits for 72 process frames, so the game time that passes depends on the frame rate. Holding ui_right for 72 frames passed headless, where the test took about half a second, and failed 3 times out of 3 under xvfb-run, where it took 110 to 178 ms and the player never reached the star. Wait for time instead: await await_millis(1200) in GdUnit4, hold_for(1.2) in GUT.
  5. simulate_action_pressed is too short for polling. GdUnit4 6.2.3's simulate_action_pressed presses and releases the action in the same call: right after it, Input.is_action_pressed("ui_right") was false, and the Star Catcher player, which reads Input.get_vector in _physics_process, moved 0 px in the next 500 ms. Use simulate_action_press, wait, then simulate_action_release.
  6. GdUnit4's Shift and Ctrl arguments are going away. GdUnit4 6.2.3 prints "Deprecated! Don't use 'shift_pressed' it will be removed in v7.0" for simulate_key_pressed(KEY_A, true), and on GdUnit4's master the same call is a parse error, "Too many arguments for "simulate_key_pressed()" call". Hold KEY_SHIFT with simulate_key_press instead.

Running headless in CI

Import the project once, then run the suite. For tests that only use keys and actions, headless is enough:

godot --headless --path . --import godot --headless --path . -s addons/gut/gut_cmdln.gd -gdir=res://test -gexit ./addons/gdUnit4/runtest.sh --godot_binary /usr/bin/godot --headless --ignoreHeadlessMode -a res://test 

GUT 9.7.1 exited 0 when every test passed and 1 when one failed. GdUnit4 6.2.3's runtest.sh exited 0 on success, 100 when a test failed, and 103 when we left out --ignoreHeadlessMode.

For tests that click Controls, drop --headless and run under xvfb-run, which starts a virtual X display, so the CI machine needs no screen:

xvfb-run -a godot --path . -s addons/gut/gut_cmdln.gd -gdir=res://test -gcompact_mode -gexit xvfb-run -a ./addons/gdUnit4/runtest.sh --godot_binary /usr/bin/godot -a res://test 

Both exited 0 with all three tests passing.

If you would rather not script inputs in a test framework, Ziva, an AI agent inside the Godot editor, has a run_scene tool that runs a scene in the open editor with timed key, action and mouse inputs and returns the runtime errors, the console output and a screenshot; Ziva's run_scene doesn't replace GUT or GdUnit4 tests and doesn't run headless in CI.

Related reading

FAQ

How do I simulate keyboard and mouse input in Godot tests (GUT or GdUnit4)?

In GUT, create GutInputSender.new(Input), queue input such as key_down(KEY_RIGHT).hold_for(1.2).wait(0.3), action_down("ui_right") or mouse_left_click_at(Vector2(350, 220)), await the sender's idle signal, then assert; call release_all() and clear() in after_each. In GdUnit4, load the scene with scene_runner("res://main.tscn") and call simulate_key_press and simulate_key_release, simulate_action_press and simulate_action_release, or set_mouse_position and simulate_mouse_button_pressed, wait with await await_millis(1200) rather than a frame count, then await runner.await_input_processed() before asserting. In our Godot 4.7.2 tests with GUT 9.7.1 and GdUnit4 6.2.3, key and action input worked headless, but clicks on a Button only worked with a display, such as xvfb-run.

Why doesn't my simulated mouse click press a Button when I run Godot tests headless?

With godot --headless, Godot 4.7.2 delivered our simulated mouse clicks to _input but never pressed a Button, in GUT 9.7.1, in GdUnit4 6.2.3 and in a plain project using Input.parse_input_event. The same tests passed under xvfb-run, a virtual X display. GdUnit4 6.2.3's own warning says "Godot 'InputEvents' are not transported by the Godot engine in headless mode". In GUT, also pass -gcompact_mode on a display, because GUT's results panel otherwise covers the game and takes the click.

How do I run GUT or GdUnit4 tests headless in CI, and what exit codes do they return?

Import once with godot --headless --path . --import. GUT 9.7.1: godot --headless --path . -s addons/gut/gut_cmdln.gd -gdir=res://test -gexit, which exited 0 when every test passed and 1 when one failed. GdUnit4 6.2.3: ./addons/gdUnit4/runtest.sh --godot_binary /usr/bin/godot --headless --ignoreHeadlessMode -a res://test, which exits 0 on success and 100 on a failed test; without --ignoreHeadlessMode, GdUnit4 6.2.3 refuses to run headless and exits 103. For tests that click Controls, run either tool without --headless under xvfb-run -a.

What is the difference between Input.parse_input_event and Input.action_press in Godot?

Input.parse_input_event feeds an InputEvent, such as an InputEventKey, through Godot's input system, so Node._input() receives it and any action mapped to that key becomes pressed. Input.action_press only marks the action as pressed: Input.is_action_pressed returns true, but Godot's docs say it "will not cause any Node._input() calls". In our Godot 4.7.2 run, a node's _input printed the parsed key press and release and nothing for action_press.