Unit Test。
堅牢なシステムを構築する上で欠かせないやつであり、また「やり方調べるのがめんどいからまた今度」と後回しされがちなやつ。
GodotにおいてはGutという、非常に使いやすいユニットテスト用ライブラリがあるのですが。
例によって日本語情報が無かったので、自分用に調べた部分を記事にしました。
Gutのインストール方法
上記のどちらかからダウンロードしてきたソースコードファイルを解凍。
内包されているaddons
フォルダごと、Gutを使用したいプロジェクトのrootディレクトリへと放り込めばオーケーです。
res://addons/gut/
というディレクトリ構造にさえなればよいので、
既に別のアドオンを導入してる方はgut
フォルダだけコピペするのでも問題ありません。
テスト用ディレクトリの作成
Gut公式ドキュメントでは、以下のディレクトリを作成することを推奨しています。
res://test
res://test/unit
res://test/integration
これら以外のディレクトリを使用することもできますが、
その場合は後述する.gutconfig
の設定値などを書き換えるする必要があります。
なので、ここではこのディレクトリ構造を前提として解説していきますので、あしからず。
テストコードの設置
res://test/unit
res://test/integration
テストコードについては、上記どちらかのディレクトリに設置します。
その際、ファイル名がtest_*.gd
といった名前になるよう、test_
というプレフィックスが必要なことに注意してください。
(ディレクトリ/プレフィックスの両方ともコンフィグから変更可能)
ここでは公式ドキュメントのSample for Setup項目に従い、
お試しとしてres://test/unit/test_example.gd
というファイルを作成。
以下コードをコピペしたことにします。
extends "res://addons/gut/test.gd"
func before_each():
gut.p("ran setup", 2)
func after_each():
gut.p("ran teardown", 2)
func before_all():
gut.p("ran run setup", 2)
func after_all():
gut.p("ran run teardown", 2)
func test_assert_eq_number_not_equal():
assert_eq(1, 2, "Should fail. 1 != 2")
func test_assert_eq_number_equal():
assert_eq('asdf', 'asdf', "Should pass")
func test_assert_true_with_true():
assert_true(true, "Should pass, true is true")
func test_assert_true_with_false():
assert_true(false, "Should fail")
func test_something_else():
assert_true(false, "didn't work")
テストコードで重要なのはこれらの点。
- 継承元として
extends "res://addons/gut/test.gd"
を記述すること - テスト対象とする関数は
test_*
という名前にすること
before_each()
やafter_each()
などの関数はテスト実行前後に自動で呼び出されるので、変数初期化などに用いるとよいでしょう。
GutのGUI要素配置
Godotエディタ側からテストを起動させたい場合は、それ用のシーンを作成する必要があります。
res://test/tests.tscn
を作成- Rootノードとして
Gut
を指定 - 描画領域内に入るよう
Gut
ノードの位置を調整
ざっくり説明するとこういった手順。
詳細な説明については公式ドキュメントのSetup項目を参照してください。
テスト実行させる際には、
エディタからres://test/tests.tscn
を実行すれば自動で行ってくれます。
ちなみに、コマンドラインからテストを実行させる場合は、
このres://test/tests.tscn
は作成せずともよさそうです。
少なくとも、僕がそうやって使用する上で問題は発生していません。
コマンドライン実行のための前準備
ここからはコマンドラインからテスト実行させる手順について。
コマンドラインからテスト実行する際には、
まずGodotのバイナリにPATHを通す必要があります。
上記リンクでは「/usr/local/bin
にgodotバイナリを置く」やり方が説明されていますが、
要はPATHさえ通ればいいので、aliasの登録でもなんでも構いません。
とにかく、godot
とタイプすればGodotバイナリが呼び出される状態にすること。
これを前提として、これ以降の解説を書いています。
もし別なる名前をalias名とした場合は、
コマンド名として記載しているgodot
をその名前で読み替えてください。
Gutのコマンドライン呼び出し
godot
と打てばGodotバイナリが呼び出せる状態になったなら、
次は「テストを実行したいプロジェクトディレクトリ」まで移動した上で、
godot -d -s --path $PWD addons/gut/gut_cmdln.gd
をコンソールで実行すればGutが呼び出されます。
ですが、これだけでは(おそらく)テストは実行されません。
「テストコードがあるディレクトリ」など、テスト実行設定がgutに指定されていないためです。
Gutコンフィグファイルを作成
テスト実行設定もろもろをコマンド起動引数から指定するやり方もあるのですが。
正直煩雑でめんどくさいし、引数をまとめてコンフィグファイルから自動読み込みする機能があるので、そちらを利用すると便利です。
デフォルトではres://.gutconfig.json
を参照する仕様なので、
そこにjsonファイルを作成して、お好みの設定値を入れるだけ。
以下コンフィグは公式ドキュメントのConfig file項目から引用。
{
"dirs":["res://test/unit/","res://test/integration/"],
"double_strategy":"partial",
"ignore_pause":false,
"include_subdirs":true,
"inner_class":"",
"log_level":3,
"opacity":100,
"prefix":"test_",
"selected":"",
"should_exit":true,
"should_maximize":true,
"suffix":".gd",
"tests":[],
"unit_test_name":""
}
これをそのままコピペするだけでも良い感じに動きます。
もしテストコードを入れるディレクトリを別なものに変更したい場合は、
dirs
配列にres://
から始まるディレクトリを入力してください。
Gut Methods
Gutには豊富な機能が取り揃えられています。
とても全部の解説は書ききれない豊富さなので、
個人的に便利だったり面白そうに感じたものだけ解説を記述しています。
assert_eq
assertionの基本形。
「入力された二つの引数が等しいか」をチェックするもの。
assert_true(a == b)
よりもテスト結果出力がわかりやすいのと、
Array同士の比較でちょっと特別なことをしているのが特徴。
記述例としてはこういった感じ。
func test_equals():
# 以下、成功するテスト
assert_eq(1 + 1, 2) # PASS
assert_eq("a", "a") # PASS
assert_eq([0, 1, 2], [0, 1, 2]) # PASS
# 以下、失敗するテスト
assert_eq(0, 1) # FAIL
assert_eq("a", "b") # FAIL
assert_eq([0, 1, 2], [3, 4, 5]) # FAIL
ぶっちゃけた話、万能なやつです。
極論、これだけ覚えれば他はほぼいらないまである。
assert_eq_deep
Gut 7.1.0
で追加されたもの。
Array
についてはassert_eq
でもわりと正確なチェックができるのですが、Dictionary
は上手くいきません。
Dictionary
は参照が比較されるため、中身が同じでも異なる参照だと失敗扱いになるためですね。
そんな扱いがちとややこしいDictionary
も正確にチェックしてくれるのが、
このassert_eq_deep
。
func test_deep_array_and_dictionary() -> void:
var array_a = [0, 1, [2, 3, [4, 5]]]
var array_b = [0, 1, [2, 3, [4, 5]]]
assert_true(array_a == array_b) # PASS
assert_eq(array_a, array_b) # PASS
assert_eq_deep(array_a, array_b) # PASS
var array_c = [{"a": 1}, {"a": 2}, {"a": 3}]
var array_d = [{"a": 1}, {"a": 2}, {"a": 3}]
assert_true(array_c == array_d) # FAIL
assert_eq(array_c, array_d) # FAIL
assert_eq_deep(array_c, array_d) # PASS
var dict_a = {"a": 1}
var dict_b = {"a": 1}
assert_true(dict_a == dict_b) # FAIL
assert_eq(dict_a, dict_b) # FAIL
assert_eq_deep(dict_a, dict_b) # PASS
覚えておくとたまに役立ちそうなやつです。
assert_typeofとassert_is
変数やオブジェクトが持ち合わせる型やクラス名をチェックする関数です。
静的型付けを行うやり方でコードを書く人なら便利に使えそうなもの。
assert_typeof
は引数にVariant.Typeの値を入れるもので、
assert_is
はクラス名を入れるもの。
役割が一部被っているようにも見えますが、
どうもassert_is
はString
やObject
などGodot built-inなクラス名を入れることができないようで、構文エラーになってしまいます。
そのため、以下のように使い分けると良いでしょう。
関数名 | 用途 |
---|---|
assert_typeof |
Array やObject など組み込み型をチェックするのに使用 |
assert_is |
Node やControl などクラスをチェックするのに使用 |
以下はコード例。
一応補足しておくと、TYPE_REAL
はfloat
型を意味しています。
func test_value_types() -> void:
var node = autofree(Node.new()) # Node value
assert_typeof(node, TYPE_OBJECT) # PASS
assert_is(node, Node) # PASS
assert_is(node, Control) # FAIL
var s = "" # string value
assert_typeof(s, TYPE_STRING) # PASS
assert_typeof(s, TYPE_INT) # FAIL
var n = 1 # int value
assert_typeof(n, TYPE_INT) # PASS
assert_typeof(n, TYPE_STRING) # FAIL
assert_typeof(n, TYPE_REAL) # FAIL
assert_signal_emitted_with_parameters
パラメータ付きsignalをテストする上で有用。
有用なんですが、ちょっと仕様に癖があるやつ……。
- signal発信前に
watch_signals()
を呼んでおく必要がある - signal付属パラメータは一つの配列にまとめられる
- 最終引数を省略すると「直近で放たれたsignalと比較」される
- 特定のsignalと比較したい場合は最終引数にindex番号を入れること
これらをしっかり守っていないとテスト失敗してしまうので、使用する際は注意が必要。
以下のサンプルコードは公式ドキュメントの同項から一部抜粋・改変したものです。
# signalを発信するだけのクラス
class SignalObject:
func _init():
add_user_signal('some_signal')
func test_assert_signal_emitted_with_parameters():
var obj = SignalObject.new()
# signal発信前にこれが必要
watch_signals(obj)
obj.emit_signal('some_signal', 1, 2, 3)
obj.emit_signal('some_signal', 'a', 'b', 'c')
obj.emit_signal('some_signal', 'one', 'two', 'three')
gut.p('-- 成功するテストここから --')
# 最終引数省略時は直近で放たれたsignalと比較
assert_signal_emitted_with_parameters(obj, 'some_signal', ['one', 'two', 'three'])
# 最終引数にindex番号を付与しているためこれも通る
assert_signal_emitted_with_parameters(obj, 'some_signal', [1, 2, 3], 0)
gut.p('-- 失敗するテストここから --')
# 存在しないsignal名を指定
assert_signal_emitted_with_parameters(obj, 'signal_does_not_exist', [])
# 紐付けが行われていないオブジェクトが指定
assert_signal_emitted_with_parameters(SignalObject.new(), 'some_signal', ['one', 'two', 'three'])
# 直近で放たれたsignalと引数が異なるため失敗
assert_signal_emitted_with_parameters(obj, 'some_signal', [1, 2, 3])
# index番号で指定したsignalと異なる引数のため失敗
assert_signal_emitted_with_parameters(obj, 'some_signal', [1, 2, 3], 1)
assert_file_exists
指定したPathにファイルが存在するかどうかをテストするやつ。
以下サンプルコードも公式ドキュメントの同項から一部抜粋し、コメント付与の改変を行ったものです。
# それぞれのテストを実行する前にファイルを生成
func before_each():
gut.file_touch('user://some_test_file')
# それぞれのテストを実行した後にファイルを削除
func after_each():
gut.file_delete('user://some_test_file')
func test_assert_file_exists():
# テスト実行時にファイルが存在しているので成功
assert_file_exists('res://addons/gut/gut.gd') # PASS
assert_file_exists('user://some_test_file') # PASS
# テスト実行時にファイルが存在していないので失敗
assert_file_exists('user://file_does_not.exist') # FAIL
assert_file_exists('res://some_dir/another_dir/file_does_not.exist') # FAIL
assert_called
Gut側オブジェクトをラッパーとして挟んでインスタンス生成することで、
「どういう関数がどういう引数付きで呼び出されたか」をテストするもの。
実に面白そうな機能なんですが、これも仕様の癖が強く……。
- テストするクラスは別ファイルになっている必要あり
- 継承されているメソッドについてはテストできない
こういった制約が存在してします。
つまり以下のようなスクリプトを別ファイルとして作成して、
# "res://test/foo_class.gd"としてファイルを保存
extends Node
var _value = 0
func get_value():
return _value
func set_value(val):
_value = val
func foo() -> void:
pass
テストコード側では、以下のように指定する必要があるわけです。
func test_assert_called() -> void:
var double_class_path = "res://test/foo_class.gd"
var doubled = double(double_class_path).new()
doubled.set_value(5)
assert_called(doubled, "set_value", [5]) # PASS
# `foo()`呼び出し前なので失敗
assert_called(doubled, "foo") # FAIL
doubled.foo()
# `foo()`呼び出し後なので成功
assert_called(doubled, "foo") # PASS
# 他の関数が呼び出された後もテスト成功となる
assert_called(doubled, "set_value", [5]) # PASS
実際に使用する上では公式ドキュメントの同項やDoublesの項をしっかり読んでおいたほうがよいでしょう。
多重継承されたメソッドを試す上ではstub
が鍵になってくるようなので、Stubbingについての記事もよさげ。
ちなみにこの記事の執筆にあたってassert_called
を僕が試したところ、
Gut 7.0.0
では上手く動かず、Gut 7.1.0
では動作しました。
ご参考までにどうぞ。
assert_accessors
いちいちテストを書くのがめんどくさいgetter/setterを、
一つのテストで同時に片付けてしまおう、という趣旨で作られたらしいやつ。
get_<PROPERTY_NAME>
という名称のメソッドが存在するかset_<PROPERTY_NAME>
という名称のメソッドが存在するか- 初めに
get_<PROPERTY_NAME>
を呼んだ際にデフォルト値を出力するか set_<PROPERTY_NAME>
で値を変更した後、get_<PROPERTY_NAME>
がその値を出力するか
これらを一度にテストできるとのこと。
仕様上、テストするgetterがget_*
という名前でいて、
またsetterがset_*
という名前である必要があります。
class FooClass:
var _value = 0
func get_value():
return _value
func set_value(val):
_value = val
func foo() -> void:
pass
func test_assert_accessors() -> void:
var foo = FooClass.new()
assert_accessors(foo, "value", 0, 1) # ALL PASS
# default値が異なる場合
assert_accessors(foo, "value", 999, 1) # 3 PASS, 1 FAIL
gut.p
テスト時にお好みのメッセージを出力できるやつ。
要するにprint
の代替版ですね。これは鉄板の便利さ。
例えばこのように記述すると、
func test_gut_print() -> void:
gut.p("-- 絶対に成功するテストここから --")
assert_true(true)
gut.p("-- 絶対に成功するテストここまで --")
こうしたテスト結果出力がコンソールになされます。
* test_gut_print
-- 絶対に成功するテストここから --
[Passed]
-- 絶対に成功するテストここまで --
autofreeとautoqfree
テスト終了時に自動でObject.free()
、
またはNode.queue_free()
を呼び出してくれるやつです。
これはテストする上で有用というより、
テスト時に警告表示されないようにするために有用なもの。
例えば、以下のテストコードを実行した場合、
func test_define_node() -> void:
var node = Node.new()
assert_is(node, Node)
このように叱られてしまいます。
[Orphans]: 1 new orphan(total).
Note: This count does not include GUT objects that will be freed upon exit.
It also does not include any orphans created by global scripts
loaded before tests were ran.
Total orphans = 2
テストコード内でインスタンス生成したNode
が解放されていないのが、叱られてしまった理由。
そのため、こう書き換えると警告表示はでなくなります。
func test_define_and_free_node() -> void:
var node = Node.new()
assert_is(node, Node)
node.free()
ですが、全部をいちいち出しては消し出しては消しするのは面倒くさい。
そういった時こそ、autofree()
の出番。
func test_define_and_autofree_node() -> void:
var node = autofree(Node.new())
assert_is(node, Node)
この記述でも警告表示はなされません。
警告出力が気になるタイプの人は、
インスタンス生成時にautofree()
かautoqfree()
で囲う癖付けをしておくとよいでしょう。
知っておくと良いかもしれない小ネタ集
assertion系関数の最終引数について
殆どのassertion関数は最終引数にString
を入れることで、
テスト結果と同時に出力させることができます。
assert_eq
などはまだしも、assert_true
やassert_false
はマジで何のテスト結果かすら表示されないので、付けておくと結果確認で大変便利。
例えばこういう感じ。
func test_add_one() -> void:
# output: "[Passed]: [2] expected to equal [2]:"
assert_eq(2, 1 + 1)
# output: "[Passed]"
assert_true(2 == 1 + 1)
# output: "[Passed]: [2] expected to equal [2]: add_one"
assert_eq(2, 1 + 1, "add_one")
# output: "[Passed]: 2 == 1 + 1"
assert_true(2 == 1 + 1, "2 == 1 + 1")
説明文章を一つ一つ書くのは面倒くさいとしても、
"2 == 1 + 1"
のように何をやってるか書くだけでも大違い。便利ですね。
しかし、Gut 7.1.0
現在、全てのassertion関数にこの引数が用意されているわけではありません
この記事で解説したやつで言うと、assert_eq_deep
あたりには無いです。
ただ、あなたがこの記事をお読みになっている時期によっては、
Gut 7.1.0
以降のアップデートで変更されているかもしれません。
その場合は「こういう時代もあった」ということで一つ。
テスト実行時のGodotウィンドウ展開を抑制したい
通常版Godotをコマンドライン起動した場合、
ウィンドウが強制展開されて実際邪魔になります。
これの対処方法は幾つか存在するのですが。
どのOSを使用しているかによって変わってくるようです。
OS | 対処方法 |
---|---|
Windows | --no-window 引数を付けてGodotを起動する |
Mac | Headless版をビルドすればよさげ? |
Linux | Linux向けHeadless版バイナリをダウンロードして使う |
※ 僕はLinux版しか使ったことがないので、他OS版については話半分でお願いします。
テストが正常終了したのにエラーコードが表示される
テストコード側に一切の問題がないにも関わらず、
以下のようなエラーが表示される場合があります。
ERROR: ~List: Condition "_first != __null" is true.
At: ./core/self_list.h:112.
ERROR: ~List: Condition "_first != __null" is true.
At: ./core/self_list.h:112.
WARNING: cleanup: ObjectDB instances leaked at exit (run with --verbose for details).
At: core/object.cpp:2135.
ERROR: clear: Resources still in use at exit (run with --verbose for details).
At: core/resource.cpp:477.
このissueページを見る限り、これはGutを開発しているbitwes氏としても理由を掴みきれてないものだそうで。
ただ、エラー表示自体は無視しても問題ないようです。
相対パスでgut_cmdln.gdを指定するとエラーコードが表示される
godot --path . -s ./addons/gut/gut_cmdln.gd
上記コマンドでgutを起動すると、以下のようなエラーコードが表示される場合があります。
SCRIPT ERROR: GDScript::reload: Parse Error: The method 'get_instance' is not present on the inferred type 'Resource' (but may be present on a subtype). (warning treated as error)
At: res://./addons/gut/gut_cmdln.gd:116.
ERROR: reload: Method failed. Returning: ERR_PARSE_ERROR
At: modules/gdscript/gdscript.cpp:599.
どうやら./addons
といったように相対パス指定してるからダメなようですけれども。
プロジェクトによっては相対パス指定でも正常動作したりするんですよね……。謎。
少なくとも僕の経験上、このエラーが表示された場合は./
を抜いて、
godot --path . -s addons/gut/gut_cmdln.gd
と表記すれば動いてくれます。
それでも動かなかったらissueに投げるしかなさそうですね。