見出し画像

Godot Unit Test (GUT) の知見をおすそ分けします

Godot Unit Test (GUT) は、Godot でユニットテストを行うためのアセットです。この記事を書いた現在のバージョンは 9.3.0 です。Godot は 4.3 です。知見が溜まってきたので、不完全ですが現状をおすそ分けします。

AssetLib から GUT (bitwes氏による) を検索して、インストールします。実際には、ダウンロード内容と場所を確認、ダウンロードして、その後、Godot エディターのメインメニューから、プロジェクト→プロジェクト設定→プラグイン を選択し、Gut の有効をオンにすれば完了です。アンインストールは、これをはずした後に addons フォルダ内の gut フォルダを丸ごと削除すれば良いようです。

その後は、res://test/unit のようにフォルダを作り、そこに新規作成したテスト用スクリプトを置きます。テスト用スクリプトには、test_ で始まるテスト用 func を置きます。具体的には、以下のようになります。(今回は、テストコード test_cases.gd とテスト対象コード main.gd の2つ)

class_name TestCases
extends GutTest

# double 用
var MainScript = null
var main_script = null
# partial_double 用
var MainScene = null
var main_scene = null


func before_all():
	MainScript = load("res://main.gd")		# script の身代わり(double)
	MainScene = load("res://main.tscn")		# scene の身代わり(partial_double)


func before_each():
	main_script = autofree(MainScript.new())				# double 生成(自動解放付き)
	main_scene = partial_double(MainScene).instantiate()	# partial_double 生成


func test_hello():
	# double
	var result: int = main_script.hello()
	assert_eq(result, 200)
	
	# partial_double
	result = main_scene.hello()
	assert_eq(result, 200)
	
	# partial_double では stub が使える
	stub(main_scene, "plus_100").to_return(500)
	result = main_scene.hello()
	assert_eq(result, 500)


# 引数群を指定することで、複数のパターンを試すテスト
var parameterized_test_params = [
	[100+100, "100+100 == 200, success"],
	[400/2, "400/2 == 200, success"],
]
func test_parameterized_test(params=use_parameters(parameterized_test_params)):
	var result = main_scene.hello()
	assert_eq(result, params[0], params[1])


func after_all():
	queue_free()	# 自身を解放して終える
class_name Main
extends Node


func hello() -> int:
	return plus_100(100)


func plus_100(value: int) -> int:
	return value + 100

ウィンドウ下部の GUT を開き、Test Directories の Include Subdirs にチェックを入れて、0 を有効にして、そこに res://test/unit を入力します。そして、Exit on Finish にチェックを入れます。最後に、Run All ボタン押下で、全テストが実行できます。Current: で表示されているスクリプト名を押せば、個別にテストを実行することもできます。

以上です。ご活用ください!

※公式ドキュメントはこちら
Gut Docs https://gut.readthedocs.io/

2024/09/15a 追記: double 生成が上手くいっていないのが課題ですが、それでも stub が使えない点を除けば、テスト対象がスクリプトだけの場合でも、おおむね予想通りの動きをします。

2024/09/15b 追記: signal のテストも書いて、新作の実動サンプルコードを用意しましたのでどうぞ。(使用に関して私は一切の責任を持ちません。また、GUT は The MIT License (MIT) Copyright (c) 2018 Tom "Butch" Wesley です)

2024/09/18a 追記: partial_double でも、テスト対象内では、子 Node の要素には $NodeName.field といった感じに、直にアクセスしないと上手くいかないみたいです。テストだと _ready() が呼ばれておらず、@onready な行が期待通りに動いていないようです。$NodeName.field だと上手くいきました。

@onready var node_name: NodeName = $NodeName

# テスト対象
func hello():
    node_name.field = "hello"    # NG    # node_name が null なので NG
    $NodeName.field = "hello"    # OK

2024/09/18b 追記: 上記を考慮した、サンプルの最新版(sandboxgut_20240918b.zip)を公開します。それと上方の前バージョンに問題があったので、ほぼ同じ内容で差し替えました(→sandboxgut_20240915c.zip)、すみません。(どちらのバージョンも、使用に関して私は一切の責任を持ちません。また、GUT は The MIT License (MIT) Copyright (c) 2018 Tom "Butch" Wesley です)

2024/09/25b 追記: GUT では _ready() を呼んでくれないので、_ready() の処理を reset() に切り出して、before_each() で呼ばせるんですが、このとき _ready() 内(reset()内)で Control.grab_focus() するとエラーが出ます。この代替案で、grab_focus.call_deferrred() として呼ぶ必要があります。情報源はこちら ↓ 

# 例

func _ready() -> void:
	reset()


# テストでも呼ばれることを想定した、初期化メソッド
func reset() -> void:
	print("in ready.")
	$ServerButton.grab_focus.call_deferred() # grab_focus を _ready() で呼ぶときは、call_deferred() を付ける

2024/09/25c 追記: $NodeName を直接指定しないために、@onready を使用せずに、reset() 内で代入しておくという代替案があります。どちらが良いかは、まだ GDScript の経験が浅いため判断がつきません。

#...

var user_line_edit: LineEdit
var password_line_edit: LineEdit
var error_label: Label
var login_button: Button

#...

func _ready() -> void:
	reset()


func reset() -> void:
	user_line_edit = $UserLineEdit
	password_line_edit = $PasswordLineEdit
	error_label = $ErrorLabel
	login_button = $LoginButton
	
#...

2024/09/28d 追記: Double と PartialDouble は stub のためにあるのだと分かってきた。Double 対象がローカル変数すら正しく返さないのは、stub のためだけにあるからみたい。だから stub 使わないなら、通常の new() や instantiate() で良い (autofree()を使う)。あと、add_child() (&remove_child()) と (InputSender の) idle シグナルを使うと _ready() や @onready の行を実行してもらえるみたい (add_child_autofree() というのもある)。まだはっきり分かっていないけれど、そのうち記事出せそうなら出します。
あと、バージョンが古いですが、作者さんが YouTube に動画も出しています。分かりづらいですが、ドキュメントにリンクがありました。↓

2024/09/29b タイトル&本文変更 お裾分け→おすそ分け

2024/09/30a 追記: まだ新記事を出せなさそうなので、とりあえず新しいサンプルコードを作成しました。GitHub にあります。今回から、GUT はご自身で用意していただくようにしました。よろしくお願いします。

2024/10/01a 追記: 自動入力を使用したインテグレーションテストのサンプルコードを作成しました。GitHub に、更新した新しいバージョンが置いてあります。clone 等してください。よろしくお願いします。

2024/10/02a 修正: Godot Unit Testing → Godot Unit Test

2024/10/02b 追記: 自動マウス操作でボタンを押すインテグレーションテストのサンプルコードを作成しました。GitHub に置いてあるものを更新です。clone や Zip ダウンロード等してください。よろしくお願いします。

2024/10/03b 追記: 操作可能なキャラクターと地面にキー押下入力を与えた、インテグレーションテストのサンプルコードを作成しました。GitHub に更新して置いてあります。これで、簡単なテストなら大体は自動テストできるかなあと思います。ここまで、ありがとうございました。

2024/10/03c 追記: 今回のコードはこんな感じです。
2024/10/04d add_child() に関して等、即席でコメント増強しました。

# MyCharacter と MyGround と キー入力 の、インテグレーションテスト(統合テスト)
extends GutTest

# テスト対象をそれぞれ設置した、テスト用シーン
const TEST_SCENE_PATH = "res://test/resources/test_my_character_moving_platform.tscn"

var TargetSceneMyCharacterMovingPlatform
var target_scene_my_character_moving_platform
var _sender = InputSender.new(Input)	# 自動入力を使う


func before_all():
	TargetSceneMyCharacterMovingPlatform = load(TEST_SCENE_PATH)


func before_each():
	# partial_double は自動解放される
	target_scene_my_character_moving_platform = partial_double(TargetSceneMyCharacterMovingPlatform).instantiate()
	
	# add_child() すれば、_ready() や @onready 行が呼ばれる
	add_child(target_scene_my_character_moving_platform)
	
	# リセット処理
	Global.cold_reset()
	Global.warm_reset()


func after_each():
	# 自動入力イベントを全リセット
	_sender.release_all()
	_sender.clear()
	
	# add_child と対
	remove_child(target_scene_my_character_moving_platform)


func after_all():
	queue_free()	# 自身を解放して終える (これで Orpahns 1 が無くなる)

#---

func test_my_character_moving():
	# 少し浮いた状態で、MyCharacter がスポーンする
	var pos_x = target_scene_my_character_moving_platform.get_node('MyCharacter').position.x as int
	var pos_y = target_scene_my_character_moving_platform.get_node('MyCharacter').position.y as int
	
	_sender.wait_frames(3)
	await _sender.idle	# 入力イベントの消化を待つ
	_sender.release_all()
	# 検査: 浮いた状態から始まり、少しの間、落ちる
	assert_gt(target_scene_my_character_moving_platform.get_node('MyCharacter').velocity.y, 0.0)
	
	_sender.wait_secs(3)
	await _sender.idle	# 入力イベントの消化を待つ
	_sender.release_all()
	# 検査: すでに着地しており、地面から落ちていない
	assert_eq(target_scene_my_character_moving_platform.get_node('MyCharacter').velocity.y, 0.0)
	
	# 3秒 右キー押下し続ける
	_sender.action_down('ui_right').hold_for(3)
	await _sender.idle	# 入力イベントの消化を待つ
	_sender.release_all()
	
	# 検査: 地面から落ちて落下している
	assert_gt(target_scene_my_character_moving_platform.get_node('MyCharacter').position.x as int, pos_x)
	assert_gt(target_scene_my_character_moving_platform.get_node('MyCharacter').position.y as int, pos_y)
	assert_gt(target_scene_my_character_moving_platform.get_node('MyCharacter').velocity.y, 0.0)
	
	await wait_seconds(1)	# 結果が見えるように少し待機


いいなと思ったら応援しよう!

那須G
ご支援いただけると今後も情報を公開していきやすくなるため、よろしければ、ぜひご検討下さい!