概要
動作確認環境: Godot 4.3+ / GUT 9.x
ゲーム開発を進めていると、こんな経験をしたことはないでしょうか?
- 敵のHPロジックを直したら、なぜかプレイヤーのダメージ計算まで壊れていた
- リファクタリングした後、どこが影響を受けたか全部手動で確認するのが大変すぎる
- 「この関数、変更しても大丈夫だっけ?」と不安で手を出せない
ユニットテスト は、こうした問題を解決する仕組みです。コードの「期待する動作」を自動的に検証するテストコードを書いておけば、変更のたびにテストを実行するだけで「壊れていないか」を即座に確認できます。
ユニットテストとは?
ユニットテストとは、プログラムの最小単位(関数やメソッド)が期待通りに動作するかを自動的に検証するテストのことです。手動で毎回ゲームを起動して確認する代わりに、テストコードがボタンひとつで検証を実行してくれます。
手動テスト:
ゲーム起動 → 敵に攻撃 → HPが減ったか目視確認 → 次のケース…(繰り返し)
ユニットテスト:
テスト実行ボタンを押す → 全ケースが数秒で自動検証 → 結果レポート表示
GUTとは?
GUT (Godot Unit Test) は、Godot専用に開発されたユニットテストアドオンです。GDScriptとほぼ同じ構文でテストを書けるため、新しい言語やツールを覚える必要がありません。
GUTが提供する主な機能:
- アサーション: 値の比較、null チェック、コンテナの要素確認
- シグナルテスト: Godot特有のシグナル発火を検証
- シーンテスト: .tscnファイルをロードしてノードツリーを検証
- モック/スタブ: 依存関係を分離してテストを独立させる
- GUTパネル: エディタ内からワンクリックでテスト実行
- コマンドライン実行: CI/CDパイプラインへの統合
tips: GUTはバージョンによってAPIや設定パスが異なる場合があります。本記事はGUT 9.x系を前提としています。AssetLibraryから最新版をインストールしてください。
GUTのインストール
それでは、まずGUTをプロジェクトに導入しましょう。GUTはGodot公式のAssetLibraryから数ステップでインストールできます。外部ツールのインストールやコマンドライン操作は不要で、エディタ内だけで完結します。
-
AssetLibraryから導入:
- Godotエディタで「AssetLib」タブを開く
- "GUT"で検索し、「Godot Unit Test (GUT) 」をインストール
- プロジェクトに
addons/gut/フォルダが追加される
-
プラグインの有効化:
- 「プロジェクト」→「プロジェクト設定」→「プラグイン」タブ
- "Gut"にチェックを入れる
-
テストフォルダの作成:
- プロジェクトルートに
test/フォルダを作成 - この中にテストファイルを配置していく
- プロジェクトルートに
-
GUTパネルの設定:
- プラグインを有効化すると、エディタ下部に「GUT」タブが表示される
- テストディレクトリ: GUTパネル上部の設定で、テストファイルの検索先を
res://test/に設定する - ファイルプレフィックス: デフォルトでは
test_プレフィックスが付いたファイルのみが検出される。GUTパネルの「Prefix」設定で変更可能 - 設定が完了したら、パネル上の「Run All」ボタンでテストを実行できる
tips: テストが一覧に表示されない場合は、GUTパネルのディレクトリ設定とファイルプレフィックスを確認してください。デフォルトの検索ディレクトリは
res://testです。
テストファイルの作成
GUTの準備ができたら、最初のテストを書いてみましょう。テストコードは通常のGDScriptとほぼ同じ構文で書けるので、GDScriptを使い慣れていればす ぐに馴染めるはずです。
以下はプレイヤーのHP管理をテストする例です。コードの各部分について、後で詳しく説明します。
# test/unit/test_player.gd
extends GutTest
# テスト対象のスクリプトをプリロード
const Player = preload("res://player.gd")
# test_で始まるメソッドが自動的にテストとして認識される
func test_player_starts_with_full_health():
var player = Player.new()
add_child_autofree(player)
assert_eq(player.health, 100, "初期体力は100であるべき")
func test_player_takes_damage():
var player = Player.new()
add_child_autofree(player)
player.take_damage(30)
assert_eq(player.health, 70, "30ダメージ後の体力は70であるべき")
func test_player_cannot_go_below_zero_health():
var player = Player.new()
add_child_autofree(player)
player.take_damage(150)
assert_eq(player.health, 0, "体力は0未満にならないべき")
コードの構造を理解しよう:
extends GutTest: テストファイルは必ずこのクラスを継承します。これによりassert_eq()やwatch_signals()などのテスト用関数が使えるようになりますpreload(): テストしたいスクリプトを事前に読み込みます。読み込んだクラスは.new()でインスタンスを作成できますtest_プレフィックス: GUTはこのプレフィックスが付いたメソッドだけを自動的にテストとして認識・実行します。プレフィックスがないメソッドはヘルパー関数として扱われ、テストとしては実行されませんadd_child_autofree(): 後述しますが、Nodeを継承するオブジェクトのテストに必要ですassert_eq(a, b, msg): 「aとbが等しいはず」という期待を表現します。第3引数のメッセージは省略可能ですが、テストが失敗したときに原因を素早く特定できるので記述を推奨します
テストを実行すると、全テストがパスすれば緑色の成功表示、1つでも失敗すれば赤色のエラー表示と該当箇所が報告されます。
add_child_autofree() はいつ必要?
テストコードを書くとき、最初に迷うのが「add_child_autofree() を呼ぶべきかどうか」です。判断基準はシンプルで、テスト対象がNodeを継承しているかどうか で決まります。
Nodeを継承するクラス(CharacterBody2D、Sprite2D など)は、シーンツリーに追加されないと _ready() が呼ばれません。テスト内で _ready() の初期化処理に依存している場合、add_child_autofree() がないとテストが正しく動作しない原因になります。
# ✅ Node継承クラス → add_child_autofree() が必要
# _ready()が呼ばれ、シーンツリーに正しく参加する
func test_player_health():
var player = Player.new() # CharacterBody2Dを継承
add_child_autofree(player) # これがないと_ready()が呼ばれない
assert_eq(player.health, 100)
# ✅ RefCounted / Resource継承クラス → add_child不要
# シーンツリーに参加しないオブジェクトなのでそのまま使える
func test_inventory_is_empty():
var inventory = Inventory.new() # RefCountedを継承
assert_true(inventory.is_empty())
| テスト対象の基底クラス | add_child_autofree | 理由 |
|---|---|---|
| Node, Node2D, CharacterBody2D 等 | 必要 | _ready() の実行とメモリ自動解放のため |
| RefCounted, Resource | 不要 | シーンツリーに依存しない。参照カウントで自動解放 |
tips: 迷ったら
add_child_autofree()を付けておけば安全です。Nodeを継承していないオブジェクトに対して呼んでもエラーにはなりません(ただしNodeのみシーンツリーに追加されます)。
アサーション関数
テストの核となるのがアサーション関数です。「この値は100のはず」「このリストにはswordが含まれているはず」といった期待をコードで表現できます。アサーションが期待通りでなければテストは 失敗 として報告され、何がどう違ったかがGUTパネルに表示されます。
GUTは値の比較、nullチェック、コンテナの要素確認など、多彩な関数を用意しています。
| 関数 | 説明 |
|---|---|
assert_eq(a, b, msg) | aとbが等しいことを確認 |
assert_ne(a, b, msg) | aとbが異なることを確認 |
assert_true(val, msg) | valがtrueであることを確認 |
assert_false(val, msg) | valがfalseであることを確認 |
assert_null(val, msg) | valがnullであることを確認 |
assert_not_null(val, msg) | valがnullでないことを確認 |
assert_gt(a, b, msg) | aがbより大きいことを確認 |
assert_lt(a, b, msg) | aがbより小さいことを確認 |
assert_has(container, val, msg) | コンテナにvalが含まれることを確認 |
assert_does_not_have(container, val, msg) | コンテナにvalが含まれないことを確認 |
すべてのアサーション関数の最後の引数 msg はオプションですが、テスト失敗時に「何を検証しようとしていたか」がすぐにわかるため、記述しておくことを推奨します。
実用例: インベントリシステムのテストで、複数のアサーションを組み合わせてみましょう。
func test_inventory_system():
var inventory = Inventory.new()
# 初期状態のチェック
assert_true(inventory.is_empty(), "初期状態は空であるべき")
assert_eq(inventory.item_count(), 0, "アイテム数は0")
# アイテム追加
inventory.add_item("sword")
assert_false(inventory.is_empty(), "アイテム追加後は空でない")
assert_has(inventory.items, "sword", "swordが含まれる")
# 所持上限チェック
for i in range(10):
inventory.add_item("potion")
assert_lt(inventory.item_count(), 100, "所持上限未満")
シグナルのテスト
値の検証に続いて、Godotならではのテスト対象であるシグナルを扱ってみましょう。Godotではノード間の通知にシグナルが多用されます。「プレイヤーが倒れたときに died シグナルが発火するか」「スコアが加算されたときに正しい値が通知されるか」といった動作も、GUTでしっかりテストできます。
シグナルテストの流れは3ステップです:
watch_signals(obj)で対象オブジェクトのシグナルを監視開始- シグナルを発火させる操作を実行
assert_signal_emitted()で発火を検証
func test_player_emits_died_signal():
var player = Player.new()
add_child_autofree(player)
# 1. シグナルの監視を開始
watch_signals(player)
# 2. シグナルを発火させる操作
player.take_damage(999)
# 3. シグナルが発火したことを確認
assert_signal_emitted(player, "died", "diedシグナルが発火すべき")
シグナルにパラメータが付いている場合は、assert_signal_emitted_with_parameters() でその値も検証できます。パラメータは配列で渡す点に注意してください。
func test_score_changed_signal_with_parameter():
var game_manager = GameManager.new()
add_child_autofree(game_manager)
watch_signals(game_manager)
game_manager.add_score(100)
# パラメータ付きシグナルの検証(パラメータは配列で渡す)
assert_signal_emitted_with_parameters(
game_manager,
"score_changed",
[100],
"score_changedが100で発火すべき"
)
シーンのテスト
ここまではスクリプト単体をテストしてきましたが、実際のゲームでは複数のノードがシーンツリーの中で連携して動作します。「Sprite2DノードがちゃんとPlayerの子にいるか」「メインメニューのStartButtonが正しく機能するか」といったことも検証しておくと、シーン構成の変更による意図しない破壊を防げます。
.tscn ファイルを load() して instantiate() すれば、テスト内で実際のノードツリーをそのまま操作できます。
func test_player_scene_initial_state():
# シーンをロードしてインスタンス化
var player_scene = load("res://scenes/player.tscn")
var player = player_scene.instantiate()
add_child_autofree(player)
# ノード構成を検証
assert_not_null(player.get_node("Sprite2D"), "Sprite2Dが存在する")
assert_not_null(player.get_node("CollisionShape2D"), "CollisionShape2Dが存在する")
assert_eq(player.position, Vector2.ZERO, "初期位置は(0,0)")
func test_ui_button_functionality():
var ui_scene = load("res://scenes/main_menu.tscn")
var ui = ui_scene.instantiate()
add_child_autofree(ui)
var start_button = ui.get_node("StartButton")
watch_signals(start_button)
# ボタンクリックをシミュレート
start_button.emit_signal("pressed")
assert_signal_emitted(start_button, "pressed", "ボタンが押されるべき")
シーンテストでは必ず add_child_autofree() を使いましょう。instantiate() で作成したノードはシーンツリーに追加しないと _ready() が呼ばれず、テスト終了後にメモリリークの原因にもなります。autofree を付けておけば、テスト終了時に自動で queue_free() されるので安全です。
セットアップとティアダウン
ここまでのテストでは毎回 Player.new() を書いていましたが、テストの数が増えるとこの繰り返しが気になってきます。同じ準備コードを10個のテストメソッドにコピーするのは冗長ですし、変更時にすべてを修正する手間も増えます。
GUTでは before_each と after_each を使って、各テストの前後に共通処理を自動実行できます。
extends GutTest
var player: Player
# 各テストの実行前に自動的に呼ばれる
func before_each():
player = Player.new()
add_child_autofree(player)
player.health = 100
# 各テストの実行後に自動的に呼ばれる
func after_each():
# クリーンアップ処理(必要に応じて)
pass
func test_player_attack():
# playerはbefore_each()で既に初期化済み
player.attack()
assert_true(player.is_attacking, "攻撃状態になる")
func test_player_defend():
player.defend()
assert_true(player.is_defending, "防御状態になる")
各テストの実行順序のイメージは次の通りです:
before_each() → test_player_attack() → after_each()
before_each() → test_player_defend() → after_each()
before_each が毎回新しいインスタンスを作るので、あるテストで player の状態を変更しても、次のテス トには影響しません。
before_all / after_all
GUTにはテストクラス全体で1回だけ実行される before_all / after_all もあります。大きなリソースの読み込みなど、毎テストで繰り返したくない重い初期化処理に便利です。
# テストクラス全体で1回だけ実行
func before_all():
print("テストクラス開始")
func after_all():
print("テストクラス終了")
| コールバック | 実行タイミング | 用途 |
|---|---|---|
before_all | テストクラスの最初に1回 | 重いリソースの読み込み、共有データの準備 |
before_each | 各テストメソッドの前 | テストごとの初期化、インスタンス生成 |
after_each | 各テストメソッドの後 | テストごとのクリーンアップ |
after_all | テストクラスの最後に1回 | 共有リソースの解放 |
モック/スタブの活用
テストを書いていると、「この関数は外部APIに依存している」「敵AIのテストにはプレイヤーの存在が必要」といった依存関係の問題にぶつかります。たとえば、ショップの割引計算をテストしたいのに、テストのためだけにデータベースやネットワークを用意するのは大変です。
モック とは、本物のオブジェクトの代わりにテスト用の「偽物」を作る仕組みです。GUTの double() を使えば、特定のメソッドの戻り値を差し替えたり、メソッドが呼ばれたかを検証したりできます。
基本的なモックの作成
func test_enemy_uses_attack_when_in_range():
# Enemyクラスのモック(テスト用の偽物)を作成
var enemy = double(Enemy).new()
add_child_autofree(enemy)
# get_distanceが常に10.0を返すように設定(スタブ)
stub(enemy, "get_distance").to_return(10.0)
enemy.update_ai(0.1)
# attack()が呼ばれたことを検証
assert_called(enemy, "attack")
スタブの使い方
stub() は特定のメソッドの戻り値を固定する機能です。「このメソッドは常にこの値を返す」と宣言することで、外部依存を排除してテストしたいロジックだけに集中できます。
func test_shop_calculates_discount():
var shop = double(Shop).new()
# 「常にVIP会員」として扱う
stub(shop, "is_vip_member").to_return(true)
# 「所持金は常に1000」として扱う
stub(shop, "get_player_gold").to_return(1000)
var price = shop.calculate_price("sword")
assert_lt(price, 100, "VIP割引が適用されるべき")
この例では、実際のプレイヤーデータやセーブデータを用意しなくても、is_vip_member() や get_player_gold() の戻り値を固定することで、割引計算ロジックだけを独立してテストできています。
主なモック/スタブ関数
| 関数 | 説明 |
|---|---|
double(クラス) | クラスのモックを作成 |
stub(obj, "method").to_return(val) | メソッドの戻り値を固定 |
assert_called(obj, "method") | メソッドが呼ばれたことを検証 |
assert_not_called(obj, "method") | メソッドが呼ばれていないことを検証 |
assert_call_count(obj, "method", count) | メソッドの呼び出し回数を検証 |
ベストプラクティス
テストの書き方は一通り把握できたと思います。ここでは、テストコードを長期的に保守しやすく保つためのポイントをまとめておきましょう。
| 推奨事項 | 説明 |
|---|---|
| 1テスト1検証 | 1つのテストで1つの機能だけを検証する |
| テスト名を明確に | test_what_should_happen_when_condition 形式で命名 |
| AAA原則 | Arrange(準備)→Act(実行)→Assert(検証)の順に書く |
| autofreeを使う | add_child_autofree() でメモリリーク防止 |
| シグナルを監視 | 重要なイベントはシグナルでテストする |
| モック/スタブを活用 | double() と stub() で依存関係を分離する |
| CI/CDに統合 | 自動テストでデグレードを早期発見 |
特に AAA原則 はテストの読みやすさに直結します。以下のようにコメントで区切ると、「何を準備して、何を実行して、何を確認しているか」が一目でわかります。
func test_player_heals_correctly():
# Arrange(準備)
var player = Player.new()
add_child_autofree(player)
player.health = 50
# Act(実行)
player.heal(30)
# Assert(検証)
assert_eq(player.health, 80, "50+30で体力は80になるべき")
コマンドライン実行例:
# ヘッドレスモードでテスト実行(パスはGUTバージョンにより異なる場合があります)
godot --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/unit
まとめ
- GUT はGodot専用のユニットテストアドオン。GDScriptと同じ構文でテストを書ける
- テストファイルは
extends GutTestを継承し、test_プレフィックスで記述 - Node継承クラス のテストには
add_child_autofree()が必要。RefCounted/Resourceには不要 - アサーション関数 で期待値を検証(
assert_eq,assert_trueなど) - シグナルテスト は
watch_signals()とassert_signal_emitted()で実装 - シーンテスト では
add_child_autofree()でメモリ管理を自動化 - before_each/after_each で各テストの共通処理、 before_all/after_all でクラス全体の処理を実行
- double()/stub() で依存関係を分離し、テストを独立させる