Godot + Gutでユニットテストを行う

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エディタ側からテストを起動させたい場合は、それ用のシーンを作成する必要があります。

  1. res://test/tests.tscnを作成
  2. RootノードとしてGutを指定
  3. 描画領域内に入るよう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_isStringObjectなどGodot built-inなクラス名を入れることができないようで、構文エラーになってしまいます。

 

そのため、以下のように使い分けると良いでしょう。

関数名 用途
assert_typeof ArrayObjectなど組み込み型をチェックするのに使用
assert_is NodeControlなどクラスをチェックするのに使用

 

以下はコード例。

一応補足しておくと、TYPE_REALfloat型を意味しています。

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_trueassert_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に投げるしかなさそうですね。