imog

主にUnityとかの事を書いています

【GodotEngine】GutのDouble周りの使い方

Gut

GodotEngineではGutというユニットテスト用のプラグインがあります。 超絶便利です。

github.com

gutのアサーションなど基本的な使い方はとても良くまとめられた記事があるのでそちらを参照してください。

dettalant.com

今回はGutでテストダブルの方法だけざっくりと紹介します。

テストダブル

テストダブルは、テストを行う際にテスト対象が依存するコンポーネントを利用せずに代わりのコンポーネントを利用してテストする手法です。例えばテスト対象が通信を行うコンポーネントに依存していた場合、そのままテストを実行すると毎回通信が走ることになり結構困ります。Twitterと通信するオブジェクトだったらテストのたびにツイートしたりするかもしれません。しかしテストは書きたい。そのような場合に依存コンポーネントを何らかの手段ででっち上げて、かつそれっぽい返り値を返してくれるようなダブル(代役)を立てるのがテストダブルです。

テストダブルは、その中でも役割によって名称が変わってきます。

  • DummyObject
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

詳しく知りたい場合は、xUnit Test Patternsを読むとよいでしょう。

xUnit Test Patterns

xunitpatterns.com

GutでのDouble

じゃあGutでどうやってテストダブルするの?という話です。

基本形

var foo_double : Foo = double(Foo).new()
# var foo_script = load("res://path/foo.gd")  型を使わない場合
# var foo_double : Foo = double(foo_script).new()

# foo_double : Foo = partial_double(Foo).new() # 一部分だけのdouble

double(Foo).new()インスタンスが生成されます。この状態のインスタンスはメソッドをコールしても何もしてくれないし、何も返してくれません。ただただインスタンスがあるだけです。

逆にpartial_double(Foo).new()は、この状態だと本来のインスタンスと全く同じ挙動のdoubleを生成します。こちらは後者のstubを行って特定のメソッドだけをでっち上げるときに使います。

ちなみに、ビルトインのクラスに対してもdoubleが可能です。

var double_node : Node = double(Node).new()

全部とは言わないがだいたいできるらしい。 https://github.com/bitwes/Gut/wiki/Doubles#what-do-i-do-with-my-double

Stub

都合のいい値を返してくれるようなダブルを「スタブ(stub)」と呼びます。

gutではstubメソッドで、特定のメソッドが呼ばれたときの返り値をでっち上げることができます。

var foo_double : Foo = double(Foo).new()

stub(foo_double, "get_foo").to_return("foo") # get_foo() が呼ばれたら "foo" を返す
gut.p(foo_double.get_foo()) # => "foo"

ちなみに引数ありのメソッドの場合は、特定の引数のときのみ返すという絞り込んだstubが行えます。

stub(foo_double, "get_foo_by_arg").to_return("foo").when_passed(10) # 引数が10で呼ばれたときのみfooを返す
gut.p(foo_double.get_foo_by_arg(10)) # => "foo"
gut.p(foo_double.get_foo_by_arg(50)) # => NULL

when_passed メソッドでパラメータまで指定できます。

テスト対象にstubしたオブジェクトを渡すことで、対象が受け取るデータをこちらでコントロールできるようになり、テストケースの前提条件を用意しやすくなります。

Spy

テスト対象の処理を経由して、特定のインスタンスのメソッドがコールされたか?という観点で検証を行いたいときがあります。そのようなダブルを「スパイ(spy)」と呼びます。

var bar : Bar = double(Bar).new()
var foo := Foo.new(bar)

foo.do_foo() # この中でbar.do_bar()が呼ばれていてほしい
assert_called(bar, "do_bar")

assert_calledは指定したオブジェクトのメソッドが呼ばれているかを検証します。例の場合はbar.do_bar()が1度でも呼ばれていればOKです。

引数の検証まで行う場合は次のように記述します。

assert_called(bar, "do_bar_with_parameter", [10]) # do_bar_with_parameter(10) と呼ばれたか

ユースケース

簡単な例ですが、シューティングゲームっぽいケースを考えてみます。自機が存在して、ショットボタンを押したら弾が出る、といった感じです。

登場人物は次の3人です。

  • MyGamePad: 入力を待つクラス
  • BulletEmitter: 弾を生成するクラス
  • Player: 上記を扱うクラス。今回のテスト対象

my_game_pad.gd

extends Reference
class_name MyGamePad

func is_shot() -> bool:
    return Input.is_action_pressed("ui_accept")

bullet_emitter.gd

extends Node2D
class_name BulletEmitter

func shot(direction_degree : int):
    # 弾が生成されるなにか
    pass

player.gd

extends KinematicBody2D
class_name Player

var _pad : MyGamePad
onready var emitter : BulletEmitter = $Emitter

func _init(pad : MyGamePad):
    _pad = pad

func _process(delta):
    # ショットキー押してたら弾を出す的な
    if _pad.is_shot():
        emitter.shot(rotation_degrees)

「ショットボタンが押された場合、弾を生成する」というテストケースを考えてみましょう。このテストを書くにあたって2つちょっと面倒ポイントが発生します。

  • ショットが押されたという前提条件をどうやって用意するか
  • 弾が生成されたことをどうやって検証するか
    • godotだったらnodeの数数えてできないこともないが今回はやめておく

前者はstubを使ってでっち上げれそうです。後者はできないこともないですが、具体的な生成処理はBulletEmitter側の責務なので、Emitterにメッセージを送れたかという検証を行ってもよさそうです。ここはspyが使えそうです。

例えば次のように書けます。

test_player.gd

extends "res://addons/gut/test.gd"

func test_player_shot_if_press_shot_button():
    var game_pad : MyGamePad = double(MyGamePad).new()
    var emitter : BulletEmitter = double(BulletEmitter).new()
    var player := Player.new(game_pad)
    add_child_autofree(player) # nodeをtreeに追加
    
    player.emitter = emitter
    stub(game_pad, "is_shot").to_return(true) # キーが押されたことにする
    
    gut.simulate(player, 1, .1)  # 擬似的に1フレーム進行させる
    assert_called(emitter, "shot", [player.rotation_degrees]) # emitterが呼ばれたことを検証する 

Playerの依存しているMyGamePadとBulletEmitterをdoubleにして渡しています。プレイヤーの入力はテスト中は取得できないので、game_pad.is_shot()をスタブすることで擬似的に「キーが押された」という状況を作り出しています。 BulletEmitterはshotをコールされても何も行いませんが、assert_calledで呼ばれたのか、パラメータは適切なものが渡されたのかを検証しています。実際にどういう風に弾を出すのかはBulletEmitter側でテストを書くとよいでしょう。 また、gut.simulateは擬似的にフレームの進行を再現するメソッドで、今回は1フレームとdelta_timeとして0.1秒を渡しています。これでplayerの_process(delta)を呼び出しています。 ちなみにadd_child_autofree はGut専用のノード追加メソッドです。このメソッド経由でaddするとGut側がテスト終了後によしなに開放してくれます。むしろ普通にadd_childすると破棄まで自分で管理する必要があるので、特に理由がないならadd_child_autofreeを使うようにしましょう。

所感

動的言語だけあってdouble周りがかなりやりやすいという印象です。継承もいらないし。 とはいえdoubleしまくって本来のテストケースを正しく検証できなくては元も子もないので、ここぞというときに使うとよいでしょう。gut.simulateのおかげで割と挙動の再現しやすいし。