コンテンツにスキップ

テスト

Behaviour のロジックを、Unity を起動せずに実行して確認できます。アップロードも Play モードも必要ありません。

既定でテストを実行するのは、Udon のセマンティクスを再現したインタプリタです。通常の C# としてコンパイルし直したものではないので、整数があふれたときの値のように C# と Udon で結果が変わる部分も、Udon 側の値になります。ただし実行環境の API はモックが答えるため、そこは実機と同じではありません。

MCP サーバからは、素の C# として実行する道も選べます(素の C# としての実行)。

確認したい Behaviour を用意します。Bump を呼ぶと value が 1 増えるだけのものです。

using Tsukimi;
public partial class TestingCounter : TsukimiBehaviour
{
public int value;
public void Bump()
{
value = value + 1;
}
public void Reset()
{
value = 0;
}
}

テストは別のファイルに書きます。元のクラスとテスト側の両方に partial を付け、テスト側のファイル名を 名前.Tests.cs にします。この名前のファイルはコンパイルの対象から外れるので、テストが配布物に混ざることも、ワールドで動作することもありません。

using Tsukimi;
public partial class TestingCounter
{
[TsukimiTest]
public void 増やすと1つ増える()
{
Bump();
Assert.AreEqual(1, value);
}
[TsukimiTest]
public void 二回増やすと2になる()
{
Bump();
Bump();
Assert.AreEqual(2, value);
Assert.IsTrue(value > 0, "増えていない");
}
}

private のフィールドやメソッドにもそのまま触れます。同じクラスの中なので、テストのために public へ開ける必要はありません。

何が起きるか動作しますが、そのメソッドはワールドにアップロードされるプログラムに含まれ、エントリポイントとして公開されます
警告inspect が TUKI0117 を出します(エラー)

[TsukimiTest] を付けた、引数の無い public void のメソッドが 1 本のテストになります。属性の付いていないメソッドは実行されないので、テストから呼ぶだけの補助メソッドを同じファイルに置けます。

メソッド名の付け方に決まりはありません。上の例のように日本語で書いても構いませんが、C# の識別子なので数字から始めることはできません。

確認は Tsukimi.Assert の静的メソッドで書きます。いま使えるのは 6 つです。

bool を 1 つ受け取り、真か偽かを確認します。条件そのものを書けるので、比較や範囲の確認はここに集まります。

[TsukimiTest]
public void 上限で止まる()
{
charge = 120;
Clamp();
Assert.IsTrue(charge <= 100);
Assert.IsFalse(charge < 0);
}

結果に出るのは真偽の値だけです。何と何を比べたかは残らないので、値の一致を見たいときは Assert.AreEqual のほうが読みやすい結果になります。

2 つの値を比べます。第 1 引数が期待した値、第 2 引数が実際の値です。この順序が結果の文面に出るので、逆にすると原因を追うときに読み違えます。

[TsukimiTest]
public void 一度触ると1つ増える()
{
Interact();
Assert.AreEqual(1, count);
Assert.AreNotEqual(0, count);
}

引数の型は object なので、数値でも文字列でも参照でも渡せます。float を直接渡すと、計算のたびに出る誤差までそのまま比べることになるため、実数の比較には向きません。

参照が null かどうかを確認します。Udon には例外がなく、null 参照にアクセスすると原因を記録せずにイベント全体が停止します。その前に検出するための Assert です。

[TsukimiTest]
public void 初期化前は空のまま()
{
Assert.IsNull(current);
Setup();
Assert.IsNotNull(current);
}

Unity のオブジェクトを渡すときは注意が必要です。破棄済みのオブジェクトは C# の null とは違う状態を取りうるので、この 2 つの結果が直感と食い違うことがあります。

メッセージつきのオーバーロード

Section titled “メッセージつきのオーバーロード”

どの Assert にも、末尾に string message を追加したオーバーロードがあります。成り立たなかったとき、その文字列がそのまま結果に出ます。

Assert.AreEqual(80, charge, "1 回触ると 20 減るはず");
Assert.IsTrue(charge > 0, "使い切っている");

同じ型の値を何度も比べるときや、条件式が長いときのように、値だけでは何を確認していたのか分からない場面で有効です。

Assert は [TsukimiTest] を付けたメソッドの中でしか書けません。外で使うとコンパイル時のエラーになります。

public void Interact()
{
// ここでは書けない(TUKI0114)。
Assert.IsTrue(count >= 0);
}

Assert の呼び出しは、実行環境に存在しない専用の命令へ変換されます。ワールドにアップロードするプログラムに残ると、到達した時点で停止します。この制限は、そうなる前にコンパイル時に捕まえるためのものです。

[Kernel] を付けたメソッドは GPU で実行されるため、テストから呼べません。Color4 や KernelId を引数や戻り値に含むメソッドも、テストからは呼べません。

確認したい計算は、float と int と bool と Vector2 だけを受け取る static のメソッドへ切り出します。カーネルとテストの両方から同じメソッドを呼べば、GPU で実行される計算そのものを Unity なしで確認できます。

public static float NextHeight(float now, float before, float around, float damping)
{
return (now * 2f - before + (around * 0.25f - now) * 0.9f) * damping;
}

切り出したメソッドがカーネルからしか呼ばれないあいだは、命令数は増えません。Behaviour 側からも呼ぶと、そのメソッドも Udon へ変換されるため命令数が増えます。

Tsukimi.Mimic を使うと、1 つのテストの中に複数のプレイヤーを置いて、同期の流れを確認できます。所有権の移動と送信は普段と同じ書き方のままで、Networking.SetOwner も RequestSerialization もそのまま書きます。Mimic が追加するのは、1 人で実行する限り書きようがない 7 つです。

書き方説明
Mimic.Join()プレイヤーが 1 人参加します。最初の 1 人が自分になり、そのプレイヤーが所有者になります
Mimic.Leave(player)そのプレイヤーが退出します。視点を置いているプレイヤーは指定できません
Mimic.Become(player)これ以降を、そのプレイヤーの側で実行します。Networking.LocalPlayer はそのプレイヤーを返し、Networking.IsOwner もそのプレイヤーを基準に答えます
Mimic.Deliver()積まれている値が、送信したプレイヤー以外の全員に届きます
Mimic.Deliver(player)積まれている値が、そのプレイヤーにだけ届きます。1 人だけが受信できないケースを再現できます
Mimic.AdvanceFrames(n)時間が n フレーム進みます。参加しているプレイヤーの Update が、参加した順に実行されます
Mimic.Explore()そのテストを、順序の定まっていない箇所をすべて入れ替えて実行します

送信側で書いた値が受信側に届くまでを、1 本のテストで確認します。

using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public partial class TestingSyncCounter : TsukimiBehaviour
{
[UdonSynced] public int count;
public void Bump()
{
count = count + 1;
RequestSerialization();
}
}
using Tsukimi;
using VRC.SDKBase;
public partial class TestingSyncCounter
{
[TsukimiTest]
public void 別のプレイヤーが書いた値が届く()
{
VRCPlayerApi me = Mimic.Join();
VRCPlayerApi other = Mimic.Join();
Networking.SetOwner(other, gameObject);
Mimic.Become(other);
count = 9;
RequestSerialization();
Mimic.Become(me);
Assert.AreEqual(0, count);
Mimic.Deliver();
Assert.AreEqual(9, count);
}
}

Mimic.Deliver の前後で値が変わります。送信した時点では受信側の値は変わらず、届いた時点で変わります。

Mimic.Explore() は、順序の定まっていない箇所をすべて入れ替えて、そのテストを繰り返し実行します。1 回の送信に含まれる複数の同期フィールドがどの順で書き込まれるかのように、実行環境が順序を保証していない箇所が対象です。

[TsukimiTest]
public void どの順で届いても矛盾しない()
{
Mimic.Explore();
// 以降は、順序を入れ替えた組み合わせのそれぞれで実行される。
}
書く位置テストの最初の文です。後ろに置くとコンパイル時のエラーになります
成り立たなかったときどの順序で成り立たなかったかが結果に出ます
実行時間すべての順序を実行するため、書かないテストより長くかかります
入れ替える箇所が無いテスト確認できることが無いため、実行できません
入れ替える箇所が多いテスト並べ替えの通り数が上限を超えると実行できません。実行する前に数えるので、待たされずに返ります

Mimic は実行環境の再現ではありません。確認できるのは記述した手順どおりに実行した結果までで、再現していない要素が 5 つあります。

再現していない要素影響
通信の遅れ届くまでの時間は測れません
送信の頻度実行環境が実際に何回送るかは分かりません
補間の途中の値Linear と Smooth の中間の値は出ません
ネットワーク越しの送信SendCustomNetworkEvent は記録されるだけで、相手には届きません。遅延呼び出し(SendCustomEventDelayedFrames など)を他の Behaviour へ向けたものも、同じく「実行不可」になります
実行環境が持つ順序Mimic.Explore() が入れ替えるのは、このツールが知っている箇所だけです。どの順序で起きても成り立つ、とはここでは言えません

test に渡したソースに他の Behaviour が含まれていると、同じ仮想シーンにインスタンスが 1 つずつ配置されます。フィールドの宣言した型が、そのうちのちょうど 1 つを指しているとき、そのフィールドはそのインスタンスへ接続されます。

Lamp.cs
using Tsukimi;
public class Lamp : TsukimiBehaviour
{
public int lit;
public void TurnOn() { lit = 1; }
}
Switch.cs
using Tsukimi;
public partial class Switch : TsukimiBehaviour
{
public Lamp lamp; // 上の Lamp へ接続される
public void Press() { lamp.SendCustomEvent(nameof(Lamp.TurnOn)); }
}
Switch.Tests.cs
using Tsukimi;
public partial class Switch
{
[TsukimiTest]
public void 押すと明かりが点く()
{
Press();
Assert.AreEqual(1, lamp.lit); // Lamp の側が実際に走った
}
}
呼んだ先その場で実行されます
相手のフィールド相手自身のものです。読み書きは相手のヒープに対して行われます
相手のコードの中の this・gameObject・transform相手を指します
GetProgramVariable と SetProgramVariableその時点の相手のヒープの値を読み書きします
接続されなかったフィールド推測せずに「実行不可」になります
書き方帰結
基底の型で宣言したフィールドどの Behaviour を指すか特定できません
同じ実行に 2 つ存在する型で宣言したフィールドどちらを指すか特定できません
配列で持った Behaviour接続されません
同じ実行に渡していない Behaviourインスタンスが存在しません
相手が同期 API を呼ぶコード相手の中の RequestSerialization・Networking.SetOwner・Mimic は実行できません。相手の同期するフィールドは、通常のフィールドとして読み書きできるだけです
テスト 1 本ごと新しいインスタンスとヒープで実行されます。前のテストが残した値は次へ持ち越されないので、結果が実行の順序に左右されません
フィールドの初期化子テストの本文へ入る前に一度実行されます。宣言に書いた値から始まるので、実行環境と同じ初期状態になります
Start などの起動時のイベントの本体自動では実行されません。実行したいときはテストの本文で Start() と書きます(テストのコードだけを読めば何が起きるか分かるようにするためです)
最初に成り立たなかった時点そのテストは打ち切られ、同じテストの中の後ろの Assert は実行されません

結果は 4 つに分かれます。

結果意味
成功最後まで実行され、どの Assert も成り立った
失敗Assert が成り立たなかった。期待した値・実際の値・ソースの位置が出る
異常終了実行の途中で停止した。ゼロ除算のように、実行環境でも同じ箇所で停止するケース
実行不可実行そのものができなかった。Assert の失敗とは別で、インタプリタがモックを持たない呼び出しを使ったときに出る。メッセージは 2 通りある。実行環境に存在する呼び出しなら、コードは正しいがここでは確認できないという意味。実行環境に存在しない呼び出しなら、その呼び出し自体を見直す。確認したい部分をその呼び出しから分離すれば実行できる場合がある

「失敗」と「実行不可」を分けているのは、直す場所が違うからです(1 つにまとめると、モックが足りないだけのものが「テストが失敗した」に見えます)。

テストの結果には、そのテストが実行した費用が付きます。上限を属性で書くと、上限を超えたテストは失敗になります。実行時間のバジェットを守りたいときに使います。

using Tsukimi;
using UnityEngine;
public partial class TestingAbsSum : TsukimiBehaviour
{
public int total;
public void Sum(int n)
{
total = 0;
for (int i = -n; i <= n; i++)
{
total = total + Mathf.Abs(i);
}
}
}
using Tsukimi;
public partial class TestingAbsSum
{
[TsukimiTest]
[CostLimit(externs: 87, milliseconds: 0.0092)]
public void 絶対値の和は110()
{
Sum(10);
Assert.AreEqual(110, total);
}
[TsukimiTest]
[CallLimit("Abs", 21)]
public void Absは21回まで()
{
Sum(10);
Assert.AreEqual(110, total);
}
}
キー意味
steps実行した命令の数
externCallsextern を呼び出した回数
estimatedMs上の 2 つに 1 回あたりの単価を掛けた、実行時間の概算(ミリ秒)
suggestedLimit実測値をそのまま引数にした CostLimit の 1 行。貼ると、そのテストの今の費用が上限になります。ミリ秒は小数 4 桁に切り上げてあり、それ以外の余裕は足されません

上の例の 1 本目を上限なしで実行したときの結果です(MCP サーバの test ツールの出力から 1 件を抜き出したもの)。

{
"name": "絶対値の和は110",
"source": "TestingAbsSum.Tests.cs",
"steps": 544,
"externCalls": 87,
"estimatedMs": 0.0091668,
"suggestedLimit": "[CostLimit(externs: 87, milliseconds: 0.0092)]",
"outcome": "passed"
}
キーが無いときその実行方法が費用を数えていないという意味です。費用が 0 という意味ではありません
費用の範囲テストが通った経路の費用です。実際に遊んだときの費用ではありません
Unity のウィンドウ各テストの結果の左に、87 ext / 0.0092 ms の形で出ます
書き方上限を置く数
[CostLimit(externs: 87)]extern を呼び出した回数
[CostLimit(milliseconds: 0.0092)]実行時間の概算
[CostLimit(steps: 544)]実行した命令の数
[CallLimit("Abs", 21)]名前に指定した文字列を含む呼び出しの回数。1 つのテストに複数付けられます
CostLimit で省いた項目その項目に上限はありません
上限を超えたとき失敗になります。failureKind が overLimit になり、expected に上限、actual に実測値が入ります
Assert が先に成り立たなかったときAssert の失敗がそのまま出ます。上限は見ません
steps に実行の歩数の上限以上を書いたとき超えることがない上限なので、実行不可になります
概算が当てにならない呼び出し生成・型での検索・文字列・配列は概算より重くなります。ミリ秒でなく externs か CallLimit で上限を置きます
素の C# としての実行CallLimit は同じように数えます。CostLimit を書いたテストと、この実行方法で数えられない呼び出しを指定した CallLimit は実行不可になります

実行する方法は 2 つあります。どちらも同じ結果を同じ場所へ書くので、片方で実行した結果をもう片方で見られます。

メニューの TsukimiCode から Tsukimi Tests を開き、すべて実行を押します。プロジェクトの中のプログラムがすべて対象になります。

結果はソースファイルごとにグループ化されて表示され、右端に 4 種類の結果のいずれかが出ます。成り立たなかったものには、期待した値・実際の値・位置がその下に付きます。

テストのウィンドウ。ソースファイルごとにグループ化され、右端に結果が出る

test ツールにソースを渡すと、同じ内容が JSON で返ります。書き方は MCP サーバ のページを見てください。

projectPath に Unity プロジェクトのパスを渡すと、結果がそのプロジェクトにも保存されます。Unity へ戻ってウィンドウを開き直すと、そこに出ます。

選び方test ツールに fast を渡します。MCP サーバからだけ選べます
向いている場面書いている途中の往復です。速いので、書いて実行して直すまでが短くなります
答える問い書いた規則が正しいかです。コンパイルした形が正しいかには答えません(その形を作らないためです)
ほかの Behaviour を一緒に渡したときこの道は同じ世界へ置かないので、Behaviour が 2 つ以上在るソースを渡すと、テストは全部「実行不可」になります。接続されるはずのフィールドが空のまま実行されることはありません
模していない呼び出し実行不可で返ります。黙って既定の値を返すことはありません
列挙型の実行時の型object へ代入した列挙型に実行時の型の名前を聞くと、この道は列挙型の名前を返し、既定の道は整数の名前を返します
長い時間のかかるテストこの道にも歩数の上限があり、超えると実行不可になります。ただし数え方が既定の道と違うので、片方で実行不可・もう片方で成功になることがあります
どちらの道だったか結果の origin に出ます。失敗を読む前にここを見ます

実行結果は、プロジェクトの Library/Tsukimi/last-test-run.json に保存されます。Library は Unity が作り直す場所なので、版管理にも配布物にも入りません。