advanced_testing - MikeSchulze/gdUnit3 Wiki

🚨 github-wiki-see.page does not render Org-Mode. Source for crawling below. Please visit the Original URL! 🚨


* Advanced Testing
This section describes how to write tests using mock's and spy's and verify the interaction on them.
For the examples, we use a simple class that implements a function `foo` that emits a signal based on an input value.

The example code can be find [[here | https://github.com/MikeSchulze/gdUnit3-examples/tree/master/]]

#+BEGIN_QUOTE
``` python
class_name ExampleWithSignal
extends Reference


signal test_signal_a
signal test_signal_b

class FooFighter:
	var _value :String
	
	func _init(value :String):
		_value = value
	
	func fight() -> String:
		return _value + " : " + "fight"

func foo(arg :int) -> void:
	if arg == 0:
		emit_signal("test_signal_a", create_fighter("a").fight())
	else:
		emit_signal("test_signal_b", create_fighter("b").fight(), true)

func create_fighter(value :String) -> FooFighter:
	return FooFighter.new(value)

#+END_QUOTE

** Using Mock This example shows how to mock a class and test function interactions on it.

You can find more detailed information about using Mocks [[here |Mocking#mocking]]

#+BEGIN_QUOTE

# GdUnit generated TestSuite
class_name ExampleMockWithSignalTest
extends GdUnitTestSuite

# TestSuite generated from
const __source = 'res://gdUnit3-examples/mocking/src/ExampleWithSignal.gd'

func test_mock_default_mode() -> void:
	# build a mock for class ExampleWithSignal (default mode RETURN_DEFAULTS)
	# default mode means no real implementation is called and a default value is returnd instead
	# this mode is usefull when you need a not fully initalisized object for testing
	# you have to override (mock) functions to return a specific value
	var mock :ExampleWithSignal = mock(ExampleWithSignal)
	
	# we did not initially have any recorded interactions on this mock
	verify_no_interactions(mock)
	
	# call function foo with value 0 
	mock.foo(0)
	
	# and we can verify we have called `foo` with argument '0'
	verify(mock).foo(0)
	
	# and no more other interactions on this mock was hapen
	verify_no_more_interactions(mock)
	
	# if you call a function a 'default' value based on return type is returnd
	assert_object(mock.create_fighter("a")).is_null()
	
	# We can 'override' a function that returns a user-defined value for a specific argument.
	# in this case for 'create_fighter("a")' and 'create_fighter("b")'
	do_return(ExampleWithSignal.FooFighter.new("my fighter a"))\
		.on(mock)\
		.create_fighter("a")
	do_return(ExampleWithSignal.FooFighter.new("my fighter b"))\
		.on(mock)\
		.create_fighter("b")
	# let check this works
	assert_object(mock.create_fighter("a")).is_equal(ExampleWithSignal.FooFighter.new("my fighter a"))
	assert_object(mock.create_fighter("b")).is_equal(ExampleWithSignal.FooFighter.new("my fighter b"))

func test_foo_emits_test_signal_a() -> void:
	# build a mock for class ExampleWithSignal and we using mode CALL_REAL_FUNC
	# on this mode the real implementaion is called
	# you can still override (mock) implementations  
	var mock :ExampleWithSignal = mock(ExampleWithSignal, CALL_REAL_FUNC)
	
	# we did not initially have any recorded interactions on this mock
	verify_no_interactions(mock)
	
	# call function foo with value 0 
	mock.foo(0)
	# we expect a signal "test_signal_a" with argument "a : fight" is emited
	verify(mock).emit_signal("test_signal_a", "a : fight")
	# we also now the code calles 'create_fighter' with arg '"a"' to build the signal
	# we can verify it by
	verify(mock).create_fighter("a")
	
	# and we expect no signal "test_signal_b" is emited
	# for simplify testing we use any() and any_bool() because we want to match 
	# for all posible signal argument combinations
	verify(mock, 0).emit_signal("test_signal_b", any(), any_bool())
	# also 'create_fighter' with arg '"b"' is never called
	verify(mock, 0).create_fighter("b")
	# and we can also verify we have called `foo` with argument '0'
	verify(mock).foo(0)
	
	
	# now we want to test emit signal with an custom fighter
	# so we have to 'override' the 'create_fighter' to return a custom value
	# return a new FooFighter when 'create_fighter("a")' is called on the mock
	do_return(ExampleWithSignal.FooFighter.new("my custom"))\
		.on(mock)\
		.create_fighter("a")
	# call again function foo with value 0 
	mock.foo(0)
	# now we expect a signal "test_signal_a" with special mocked value is called
	verify(mock, 1).emit_signal("test_signal_a", "my custom : fight")
	
	# Lastly, we want to make sure that we have no other interactions on this mock
	verify_no_more_interactions(mock)

#+END_QUOTE The example can be find [[here|https://github.com/MikeSchulze/gdUnit3-examples/blob/master/SignalsTestExamples/test/ExampleMockWithSignalTest.gd]]

** Using Spy You can find more detailed information about using spys [[here |Spy#definition]]

#+BEGIN_QUOTE

# GdUnit generated TestSuite
class_name ExampleSpyWithSignalTest
extends GdUnitTestSuite

# TestSuite generated from
const __source = 'res://gdUnit3-examples/mocking/src/ExampleWithSignal.gd'

func test_foo_emits_test_signal_a() -> void:
	# build a spy_instance from the ExampleWithSignal instance
	var spy_instance :ExampleWithSignal = spy(ExampleWithSignal.new())
	
	# we did not initially have any recorded interactions on this spy_instance
	verify_no_interactions(spy_instance)
	
	# call function foo with value 0 
	spy_instance.foo(0)
	# we expect a signal "test_signal_a" with argument "a : fight" is emited
	verify(spy_instance).emit_signal("test_signal_a", "a : fight")
	# we also now the code calles 'create_fighter' with arg '"a"' to build the signal
	# we can verify it by
	verify(spy_instance).create_fighter("a")
	
	# and we expect no signal "test_signal_b" is emited
	# for simplify testing we use any() and any_bool() because we want to match 
	# for all posible signal argument combinations
	verify(spy_instance, 0).emit_signal("test_signal_b", any(), any_bool())
	# also 'create_fighter' with arg '"b"' is never called
	verify(spy_instance, 0).create_fighter("b")
	# and we can also verify we have called `foo` with argument '0'
	verify(spy_instance).foo(0)
	
	# Lastly, we want to make sure that we have no other interactions on this spy_instance
	verify_no_more_interactions(spy_instance)

#+END_QUOTE The example can be find [[here|https://github.com/MikeSchulze/gdUnit3-examples/blob/master/SignalsTestExamples/test/ExampleSpyWithSignalTest.gd]]

** Test Scenes

To test scenes GdUnit provides a [[scene runner|SceneRunner]] where you can use to test the scene processed like at runtime.

This example shows how to spy on a scene and test input events on it. #+BEGIN_QUOTE

# GdUnit generated TestSuite
class_name GameSpyTest
extends GdUnitTestSuite

# TestSuite generated from
const __source = 'res://gdUnit3-examples/MenuDemo2D/src/Game.gd'

# enable only for visualisize the spy_scene steps
var _debug_wait = false

var spy_scene

func before():
	# using 'before()' to create only once the spy_scene at beginning of test suite run
	var scene_instance = load("res://gdUnit3-examples/MenuDemo2D/src/Game.tscn").instance()
	# create a spy on this spy_scene instance
	spy_scene = spy(scene_instance)

func before_test():
	# reset previous recoreded interactions on this mock for each test
	reset(spy_scene)

func test_game_scene_spyed():
	scene_runner(spy_scene)
	
	# check inital state 
	verify(spy_scene, 0)._on_game_paused(any_bool())
	assert_str(spy_scene._label.text).is_equal("App started")
	assert_str(spy_scene._state_label.text).is_equal("Game Init");
	assert_bool(spy_scene._game_repository.is_connected("new_game", spy_scene, "_on_new_game")).is_true()
	
	assert_bool(spy_scene._game_repository.is_connected("new_game", spy_scene, "_on_new_game")).is_true()
	assert_bool(spy_scene._game_repository.is_connected("load_game", spy_scene, "_on_load_game")).is_true()
	assert_bool(spy_scene._game_repository.is_connected("save_game", spy_scene, "_on_save_game")).is_true()

func test_game_menu_open_close():
	var scene_runner := scene_runner(spy_scene)
	
	# first esc press to open the main menu
	scene_runner.simulate_key_pressed(KEY_ESCAPE)
	verify(spy_scene)._on_game_paused(true)
	assert_str(spy_scene._state_label.text).is_equal("Game Paused")
	
	# press esc again to close the game menu
	scene_runner.simulate_key_pressed(KEY_ESCAPE)
	verify(spy_scene)._on_game_paused(false)
	assert_str(spy_scene._state_label.text).is_equal("Game Running")

func test_game_menu_new_game_esc():
	var scene_runner := scene_runner(spy_scene)
	# simulate esc pressed to open the main menu
	scene_runner.simulate_key_pressed(KEY_ESCAPE)
	yield(get_tree(), "idle_frame")
	
	# game should be paused
	assert_str(spy_scene._state_label.text).is_equal("Game Paused")
	verify(spy_scene)._on_game_paused(true)
	if _debug_wait:
		yield(get_tree().create_timer(1), "timeout")
	
	# press enter to open create new game menu
	scene_runner.simulate_key_pressed(KEY_ENTER)
	yield(get_tree(), "idle_frame")
	if _debug_wait:
		yield(get_tree().create_timer(1), "timeout")

	# press esc to exit new game menu
	scene_runner.simulate_key_pressed(KEY_ESCAPE)
	yield(get_tree(), "idle_frame")
	if _debug_wait:
		yield(get_tree().create_timer(1), "timeout")
	
	# press esc back to game
	scene_runner.simulate_key_pressed(KEY_ESCAPE)
	yield(get_tree(), "idle_frame")
	if _debug_wait:
		yield(get_tree().create_timer(1), "timeout")
	
	# check the game pause is disabled and running after the main menu is closed
	verify(spy_scene)._on_game_paused(false)
	assert_str(spy_scene._state_label.text).is_equal("Game Running")

func test_game_menu_new_game_press_new():
	var scene_runner := scene_runner(spy_scene)

	# simulate esc pressed to open the main menu
	scene_runner.simulate_key_pressed(KEY_ESCAPE)
	yield(get_tree(), "idle_frame")
	
	# game should be paused
	assert_str(spy_scene._state_label.text).is_equal("Game Paused")
	verify(spy_scene)._on_game_paused(true)
	if _debug_wait:
		yield(get_tree().create_timer(1), "timeout")
	
	# press enter to open create new game menu
	scene_runner.simulate_key_pressed(KEY_ENTER)
	yield(get_tree(), "idle_frame")
	if _debug_wait:
		yield(get_tree().create_timer(1), "timeout")

	# press again enter to  create a new game
	scene_runner.simulate_key_pressed(KEY_ENTER)
	yield(get_tree(), "idle_frame")
	if _debug_wait:
		yield(get_tree().create_timer(1), "timeout")
	
	# check a new game is created an running
	verify(spy_scene)._on_game_paused(false)
	verify(spy_scene)._on_new_game()
	assert_str(spy_scene._state_label.text).is_equal("Game Running")
	assert_str(spy_scene._label.text).is_equal("Starting a new Game")

#+END_QUOTE The example can be find [[here|https://github.com/MikeSchulze/gdUnit3-examples/blob/master/MenuDemo2D/test/GameSpyTest.gd]]

*** Simulate Scene Processing

With the [[scene runner|SceneRunner]] you can test the scene processed like at runtime. The runner provides functions to simulate the scene processing.

** Example #+BEGIN_QUOTE

Simulates scene processing for a certain number of frames by given delta peer frame

func simulate(frames: int, delta_peer_frame :float) -> GdUnitSceneRunner:

#+END_QUOTE

#+BEGIN_QUOTE

Simulates scene processing until the given signal is emitted by the scene

func simulate_until_signal(signal_name :String, arg0 = null, arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null) -> GdUnitSceneRunner:

#+END_QUOTE

Short Example:

	# run the scene for 10 frames each frame with a delta of 10ms
	yield(_runner.simulate(10, .010), "completed")

	# wait until the signal `door_closed` is emitted by the scene
	yield(_runner.simulate_until_signal("door_closed"), "completed")

You must always call the functions simulate and simulate_until_signal enclosed in a yield function! The yield is need to simulate by correct timings.

The full example can be find [[here|https://github.com/MikeSchulze/gdUnit3-examples/blob/master/RoomDemo3D/test/rooms/RoomWithDoorTest.gd]]

** Control Test Execution Time The maximum execution time for a test case is set to a maximum runtime of 5 minutes by default. If a test runs longer than 5 minutes, the test is aborted by a timeout error. You can change the default timeout under GdUnit Setting or, if you wish, set the timeout for a test separately. #+BEGIN_QUOTE

# configure the test with a timeout of 2s
func test_foo(timeout=2000):
 ...

#+END_QUOTE

** Custom Failure Report Sometimes you want to have custom failure messages to get a clearer picture of the failure.

You can achive this by simple using 'override_failure_message()'. #+BEGIN_QUOTE

This test will fail with a custom failure message

func test_override_failure_message() -> void:
	assert_str("This is a test")\
		.override_failure_message("Expecting: failure in end with 100")\
		.is_equal("Error 100")

#+END_QUOTE