テスト
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; }}テストのファイル
Section titled “テストのファイル”テストは別のファイルに書きます。元のクラスとテスト側の両方に 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 へ開ける必要はありません。
テストを本体側に書いた場合
Section titled “テストを本体側に書いた場合”| 何が起きるか | 動作しますが、そのメソッドはワールドにアップロードされるプログラムに含まれ、エントリポイントとして公開されます |
| 警告 | inspect が TUKI0117 を出します(エラー) |
テストメソッドの宣言
Section titled “テストメソッドの宣言”[TsukimiTest] を付けた、引数の無い public void のメソッドが 1 本のテストになります。属性の付いていないメソッドは実行されないので、テストから呼ぶだけの補助メソッドを同じファイルに置けます。
メソッド名の付け方に決まりはありません。上の例のように日本語で書いても構いませんが、C# の識別子なので数字から始めることはできません。
Assert
Section titled “Assert”確認は Tsukimi.Assert の静的メソッドで書きます。いま使えるのは 6 つです。
Assert.IsTrue / Assert.IsFalse
Section titled “Assert.IsTrue / Assert.IsFalse”bool を 1 つ受け取り、真か偽かを確認します。条件そのものを書けるので、比較や範囲の確認はここに集まります。
[TsukimiTest]public void 上限で止まる(){ charge = 120; Clamp();
Assert.IsTrue(charge <= 100); Assert.IsFalse(charge < 0);}結果に出るのは真偽の値だけです。何と何を比べたかは残らないので、値の一致を見たいときは Assert.AreEqual のほうが読みやすい結果になります。
Assert.AreEqual / Assert.AreNotEqual
Section titled “Assert.AreEqual / Assert.AreNotEqual”2 つの値を比べます。第 1 引数が期待した値、第 2 引数が実際の値です。この順序が結果の文面に出るので、逆にすると原因を追うときに読み違えます。
[TsukimiTest]public void 一度触ると1つ増える(){ Interact();
Assert.AreEqual(1, count); Assert.AreNotEqual(0, count);}引数の型は object なので、数値でも文字列でも参照でも渡せます。float を直接渡すと、計算のたびに出る誤差までそのまま比べることになるため、実数の比較には向きません。
Assert.IsNull / Assert.IsNotNull
Section titled “Assert.IsNull / Assert.IsNotNull”参照が 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, "使い切っている");同じ型の値を何度も比べるときや、条件式が長いときのように、値だけでは何を確認していたのか分からない場面で有効です。
使える場所の制限
Section titled “使える場所の制限”Assert は [TsukimiTest] を付けたメソッドの中でしか書けません。外で使うとコンパイル時のエラーになります。
public void Interact(){ // ここでは書けない(TUKI0114)。 Assert.IsTrue(count >= 0);}Assert の呼び出しは、実行環境に存在しない専用の命令へ変換されます。ワールドにアップロードするプログラムに残ると、到達した時点で停止します。この制限は、そうなる前にコンパイル時に捕まえるためのものです。
カーネルの計算の確認
Section titled “カーネルの計算の確認”[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() | そのテストを、順序の定まっていない箇所をすべて入れ替えて実行します |
別のプレイヤーが書いた値
Section titled “別のプレイヤーが書いた値”送信側で書いた値が受信側に届くまでを、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 の前後で値が変わります。送信した時点では受信側の値は変わらず、届いた時点で変わります。
順序の入れ替え
Section titled “順序の入れ替え”Mimic.Explore() は、順序の定まっていない箇所をすべて入れ替えて、そのテストを繰り返し実行します。1 回の送信に含まれる複数の同期フィールドがどの順で書き込まれるかのように、実行環境が順序を保証していない箇所が対象です。
[TsukimiTest]public void どの順で届いても矛盾しない(){ Mimic.Explore();
// 以降は、順序を入れ替えた組み合わせのそれぞれで実行される。}| 書く位置 | テストの最初の文です。後ろに置くとコンパイル時のエラーになります |
| 成り立たなかったとき | どの順序で成り立たなかったかが結果に出ます |
| 実行時間 | すべての順序を実行するため、書かないテストより長くかかります |
| 入れ替える箇所が無いテスト | 確認できることが無いため、実行できません |
| 入れ替える箇所が多いテスト | 並べ替えの通り数が上限を超えると実行できません。実行する前に数えるので、待たされずに返ります |
確認できる範囲
Section titled “確認できる範囲”Mimic は実行環境の再現ではありません。確認できるのは記述した手順どおりに実行した結果までで、再現していない要素が 5 つあります。
| 再現していない要素 | 影響 |
|---|---|
| 通信の遅れ | 届くまでの時間は測れません |
| 送信の頻度 | 実行環境が実際に何回送るかは分かりません |
| 補間の途中の値 | Linear と Smooth の中間の値は出ません |
| ネットワーク越しの送信 | SendCustomNetworkEvent は記録されるだけで、相手には届きません。遅延呼び出し(SendCustomEventDelayedFrames など)を他の Behaviour へ向けたものも、同じく「実行不可」になります |
| 実行環境が持つ順序 | Mimic.Explore() が入れ替えるのは、このツールが知っている箇所だけです。どの順序で起きても成り立つ、とはここでは言えません |
同じ実行に渡した他の Behaviour
Section titled “同じ実行に渡した他の Behaviour”test に渡したソースに他の Behaviour が含まれていると、同じ仮想シーンにインスタンスが 1 つずつ配置されます。フィールドの宣言した型が、そのうちのちょうど 1 つを指しているとき、そのフィールドはそのインスタンスへ接続されます。
using Tsukimi;
public class Lamp : TsukimiBehaviour{ public int lit;
public void TurnOn() { lit = 1; }}using Tsukimi;
public partial class Switch : TsukimiBehaviour{ public Lamp lamp; // 上の Lamp へ接続される
public void Press() { lamp.SendCustomEvent(nameof(Lamp.TurnOn)); }}using Tsukimi;
public partial class Switch{ [TsukimiTest] public void 押すと明かりが点く() { Press(); Assert.AreEqual(1, lamp.lit); // Lamp の側が実際に走った }}接続された相手に起きること
Section titled “接続された相手に起きること”| 呼んだ先 | その場で実行されます |
| 相手のフィールド | 相手自身のものです。読み書きは相手のヒープに対して行われます |
相手のコードの中の this・gameObject・transform | 相手を指します |
GetProgramVariable と SetProgramVariable | その時点の相手のヒープの値を読み書きします |
| 接続されなかったフィールド | 推測せずに「実行不可」になります |
接続されない書き方
Section titled “接続されない書き方”| 書き方 | 帰結 |
|---|---|
| 基底の型で宣言したフィールド | どの Behaviour を指すか特定できません |
| 同じ実行に 2 つ存在する型で宣言したフィールド | どちらを指すか特定できません |
| 配列で持った Behaviour | 接続されません |
| 同じ実行に渡していない Behaviour | インスタンスが存在しません |
| 相手が同期 API を呼ぶコード | 相手の中の RequestSerialization・Networking.SetOwner・Mimic は実行できません。相手の同期するフィールドは、通常のフィールドとして読み書きできるだけです |
実行のしくみ
Section titled “実行のしくみ”| テスト 1 本ごと | 新しいインスタンスとヒープで実行されます。前のテストが残した値は次へ持ち越されないので、結果が実行の順序に左右されません |
| フィールドの初期化子 | テストの本文へ入る前に一度実行されます。宣言に書いた値から始まるので、実行環境と同じ初期状態になります |
Start などの起動時のイベントの本体 | 自動では実行されません。実行したいときはテストの本文で Start() と書きます(テストのコードだけを読めば何が起きるか分かるようにするためです) |
| 最初に成り立たなかった時点 | そのテストは打ち切られ、同じテストの中の後ろの Assert は実行されません |
結果の読み方
Section titled “結果の読み方”結果は 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); }}結果に付く費用
Section titled “結果に付く費用”| キー | 意味 |
|---|---|
steps | 実行した命令の数 |
externCalls | extern を呼び出した回数 |
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 の形で出ます |
上限の書き方
Section titled “上限の書き方”| 書き方 | 上限を置く数 |
|---|---|
[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 つあります。どちらも同じ結果を同じ場所へ書くので、片方で実行した結果をもう片方で見られます。
Unity のウィンドウから
Section titled “Unity のウィンドウから”メニューの TsukimiCode から Tsukimi Tests を開き、すべて実行を押します。プロジェクトの中のプログラムがすべて対象になります。
結果はソースファイルごとにグループ化されて表示され、右端に 4 種類の結果のいずれかが出ます。成り立たなかったものには、期待した値・実際の値・位置がその下に付きます。

MCP サーバから
Section titled “MCP サーバから”test ツールにソースを渡すと、同じ内容が JSON で返ります。書き方は MCP サーバ のページを見てください。
projectPath に Unity プロジェクトのパスを渡すと、結果がそのプロジェクトにも保存されます。Unity へ戻ってウィンドウを開き直すと、そこに出ます。
素の C# としての実行
Section titled “素の C# としての実行”| 選び方 | test ツールに fast を渡します。MCP サーバからだけ選べます |
| 向いている場面 | 書いている途中の往復です。速いので、書いて実行して直すまでが短くなります |
| 答える問い | 書いた規則が正しいかです。コンパイルした形が正しいかには答えません(その形を作らないためです) |
| ほかの Behaviour を一緒に渡したとき | この道は同じ世界へ置かないので、Behaviour が 2 つ以上在るソースを渡すと、テストは全部「実行不可」になります。接続されるはずのフィールドが空のまま実行されることはありません |
| 模していない呼び出し | 実行不可で返ります。黙って既定の値を返すことはありません |
| 列挙型の実行時の型 | object へ代入した列挙型に実行時の型の名前を聞くと、この道は列挙型の名前を返し、既定の道は整数の名前を返します |
| 長い時間のかかるテスト | この道にも歩数の上限があり、超えると実行不可になります。ただし数え方が既定の道と違うので、片方で実行不可・もう片方で成功になることがあります |
| どちらの道だったか | 結果の origin に出ます。失敗を読む前にここを見ます |
結果の出力先
Section titled “結果の出力先”実行結果は、プロジェクトの Library/Tsukimi/last-test-run.json に保存されます。Library は Unity が作り直す場所なので、版管理にも配布物にも入りません。