Mocking Input

The GutInputSender class operates on one or more receivers. It will create and send InputEvent instances to all of its receivers.

There are two ways you could be processing your input. You could be using the _input events to receive input events and process them. The other way is to interact with the Input global and detect input in the _process and _physics_process methods. GutInputSender works with both approaches, but using GutInputSender differs for each approach. Read the sections below to learn the best way to use GutInputSender with your game.

Using an Object as a Receiver

When you use an instance of an object as a receiver, GutInputSender will send InputEvent instances to the various input methods. They will be called in this order:

  1. _input

  2. _gui_input

  3. _unhandled_input

When there are multiple receivers, each receiver will be called in the order they were added. All three _input methods will be called on each receiver then the GutInputSender will move to the next receiver.

When using objects as receivers it is recommended that each test create its own instance of GutInputSender. GutInputSender retains information about what actions/buttons/etc have been pressed. By creating a new instance in each test, you don’t have to worry about clearing this state between tests.

If you are processing input by directly interacting with the Input global, then you should follow the instructions in the next section.

func test_shoot():
    var player = Player.new()
    var sender = InputSender.new(player)

    sender.action_down("shoot")
    assert_true(player.is_shooting())

Using Input as a Receiver

When Input is used as a receiver Input will send all inputs it receives from the GutInputSender to every object that has been added to the tree. Input will treat all the events it gets exactly the same as if the events were triggered from hardware. This means all the is_action_just_pressed and similar functions will work the same. The InputEvent instances will also be sent to the various _input methods on objects in the tree in whatever order Input desires.

Using Input makes testing objects that handle input via _process or _process_delta much easier but you have to be a little careful when using it though. Since the Input instance is global and retains its state for the duration of the test run.

  1. You should declare your GutInputSender instance at the class level. You will need access to it in the after_each method.

  2. Call release_all on the GutInputSender in after_each. This makes sure that Input doesn’t think that a button is pressed when you don’t expect it to be. If Input thinks a button is pressed, it will not send any “down” events until it gets an “up” event.

  3. Call clear on the GutInputSender in after_each. This clears out any state the GutInputSender has. It tracks inputs so that functions like hold_for can create dynamic “up” events, as well as various other things. Calling clear makes sure that GutInputSender state does not leak from one test to another.

  4. You must ALWAYS await before making an assert or your objects will not get a chance to process the frame the Input was sent on (_process and _physics_process will not be called without a await).

var _sender = InputSender.new(Input)

func after_each():
    _sender.release_all()
    _sender.clear()

func test_shoot():
    var player = Player.new()

    _sender.action_down("shoot").wait_frames(1)
    await(_sender.idle)

    assert_true(player.is_shooting())

Chaining Input Events

The GutInputSender methods return the instance so you can chain multiple calls together to script out a sequence of inputs. The sequence is immediately started. When the sequence finishes the 'idle' signal is emitted.

var player = Player.new()
var sender = InputSender.new(player)

# press a, then b, then release a, then release b
sender.key_down("a").wait(.1)\
    .key_down(KEY_B).wait(.1)\
    .key_up("a").wait(.1)\
    .key_up(KEY_B)
await(sender.idle)

The GutInputSender will emit the idle signal when all inputs in a sequence have been sent and all waits have expired.

Any events that do not have a wait or hold_for call in between them will be fired on the same frame.

# checking for is_action_just_pressed for "jump" and "fire" will be true in the same frame.
sender.action-down("jump").action_down("fire")

You can use a trailing wait to give the result of the input time to play out

# wait an extra .2 seconds at the end so that asserts will be run after the
# shooting animation finishes.
sender.action_down("shoot").hold_for(1).wait(.2)
await(sender.idle)

Examples

These are examples of scripting out inputs and sending them to Input. The Player class in these examples would be handling input in _process or _process_physics.

extends GutTest

# When sending events to Input the InputSender instance should be defined at
# the class level so that you can easily clear it between tests in after_each.
var _sender = InputSender.new(Input)

# IMPORTANT:  When using Input as the receiver of events you should always
#             release_all and clear the InputSender so that any
#             actions/keys/buttons that are not released in a test are released
#             before the next test runs.  "down" events will not be sent by
#             Input if the action/button/etc is currently "down".
func after_each():
    _sender.release_all()
    _sender.clear()


# In this test we press and hold the jump button for .1 seconds then wait
# another .3 seconds for the jump to take take place.  We then assert that
# the character has moved up between 4 and 5 pixels.
func test_tapping_jump_jumps_certain_height():
    var player = add_child_autofree(Player.new())

    _sender.action_down("jump").hold_for(.1).wait(.3)
    await(_sender.idle)

    assert_between(player.position.y, 4, 5)


# This is similar to the other test but we hold jump for longer and then
# verify the player jumped higher.
func test_holding_jump_jumps_higher():
    var player = add_child_autofree(Player.new())

    _sender.action_down("jump").hold_for(.75)
    await(_sender.idle)

    assert_between(player.position.y, 7, 8)


# This tests throwing a fireball, like with Ryu or Ken from Street Fighter.
# Note that there is not a hold_for after "forward" and the key_down for
# fierce punch (FP) immediately after.  This means the "forward" motion AND
# FP are pressed in the same frame.
func test_fireball_input():
    var player = add_child_autofree(Player.new())

    _sender.action_down("down").hold_for("2f")\
        .action_down("down_forward").hold_for("2f")\
        .action_down("forward").key_down("FP")
    await(_sender.idle)

    assert_true(player.is_throwing_fireball())


# In this example we are testing that two actions in combination cause the
# player to slide.  Note that there is no release of the actions in this
# test.  This is a good example of why using release_all in after_each makes
# the tests simpler to write and prevents leaking of inputs from one test to
# another.
func test_holding_down_and_jump_does_slide():
    var player = add_child_autofree(Player.new())

    _sender.action_down("down").wait("1f")\
        .action_down("jump").wait("2f")
    await(_sender.idle)

    assert_gt(player.velocity.x, 0)

Gotchas

  • When using Input as a receiver, everything in the tree gets the signals AND any actual inputs from hardware will be sent as well. It’s best not to touch anything when running these tests.

  • If you use a class level GutInputSender and forget to call release_all and clear between tests then things will eventually start behaving weird and your tests will pass/fail in unpredictable ways.

Understanding Input.use_accumulated_input

When use_accumualted_input is enabled, Input waits to process input until the end of a frame. This means that if you do not flush the buffer or there are no “waits” or calls to await before you test how input was processed then your tests will fail.

Testing with use_accumulated_input

Other ways that aren’t so good.

If you use these approaches you should quarantine these tests in their own Inner Class or script so that they do not influence other tests that do not expect the buffer to be constantly flushed or use_accumulated_input to be disabled.

  1. In GUT 7.4.0 GutInputSender has an auto_flush_input property which is disabled by default. When enabled this will call Input.flush_buffered_events after each input sent through an GutInputSender. This is a bit dangerous since this can cause some of your tests to not test the way your game will receive input when playing the game.

  2. You can disable use_accumulated_input in before_all and re-enable in after_all. Just like with auto_flush_input, this has the potential to not test all inputs the same way as your game will get them when playing the game.

Examples

The following assume use_accumulated_input is enabled and uses Godot 3.5 syntax. In 3.4 you have to call set_use_accumulated_input. There is no way to check the value of this flag in 3.4.

extends GutTest

var _sender = InputSender.new(Input)

func before_all():
    InputMap.add_action("jump")

func after_each():
    _sender.release_all()
    _sender.clear()

func test_when_uai_enabled_input_not_processed_immediately():
    _sender.key_down('a')
    assert_false(Input.is_key_pressed(KEY_A))

func test_when_uai_enabled_just_pressed_is_not_processed_immediately():
    _sender.action_down('jump')
    assert_false(Input.is_action_just_pressed('jump'))

func test_when_uai_enabled_waiting_makes_button_pressed():
    # wait 10 frames.  In testing, 6 frames failed, but 7 passed.  Added 3 for
    # good measure.
    _sender.key_down(KEY_Y).wait('10f')
    await(_sender.idle)
    assert_true(_sender.is_key_pressed(KEY_Y))
    assert_true(Input.is_key_pressed(KEY_Y))

func test_when_uai_enabled_flushig_buffer_sends_input_immediatly():
    _sender.key_down('a')
    Input.flush_buffered_events()
    assert_true(Input.is_key_pressed(KEY_A))

func test_disabling_uai_sends_input_immediately():
    Input.use_accumulated_input = false
    _sender.key_down('a')
    assert_true(Input.is_key_pressed(KEY_A))
    # re-enable so we don't ruin other tests
    Input.use_accumulated_input = true

func test_when_uai_enabled_flushing_buffer_just_pressed_is_processed_immediately():
    _sender.action_down('jump')
    Input.flush_buffered_events()
    assert_true(Input.is_action_just_pressed('jump'))