やらなイカ?

たぶん、iOS/Androidアプリの開発・テスト関係。

生成AI時代のテストの役割

AIコーディングエージェントを足がかりに開発者テストを導入しつつあるチームと話していて、何を確認したいのかが曖昧なままテストを書こうとしているように感じました。 その背景には、テストの目的を「バグをみつけること」と狭く捉えることに起因する掘り下げ不足がありそうです。 そこで、テストの役割についてもう一歩踏み込んで考えるための知見をまとめてみました*1。

手動・自動、テストレベル(ユニット/統合/システム(E2E)テスト)に限定しない、テスト全体の話です。

テストの目的

『JSTQB認定テスト技術者資格試験シラバス Foundation LevelのVersion 2011*2』では、テスト*3の目的として次の4つが挙げられています。

  • 欠陥を摘出する
  • 対象ソフトウェアの品質レベルが十分であることを確認する
  • 意志決定のための情報を示す
  • 欠陥の作りこみを防ぐ

このうち「欠陥(バグ)を摘出する」を重視しがちですが、「対象ソフトウェアの品質レベルが十分であることを確認する」つまり要求や仕様どおりに動作することを確認し、リリース可否などの「意志決定のための情報を示す」こともテストの大きな目的です。

また、テストや品質保証活動を開発の早い段階から実施し、欠陥の作りこみを防いだり早期に摘出したりする取り組みは「シフトレフト」と呼ばれ、重要視されています*4。

目的に応じて、取れる手段を考えてみましょう。

バグの摘出にはAIエージェントによるレビューも活用できる

バグにも色々ありますが、コードの書き方や仕様書との整合性に起因するものの中には、AIエージェントによるレビューで発見できるものもあります*5。 特に、コードを実行せずに発見できる欠陥については、AIエージェントによるレビューに任せ、動的テストを省略するという判断もできます。 投資対効果(ROI)を考えれば、検証観点によってはレビューで代替する選択肢もあってよいのではないでしょうか。

ただし、AIに参照させる仕様書が整備されていることが前提ですし、LLMは確率的な性質をもつため、実行ごとに指摘内容が変わる不安定さを許容しなければなりません。

また、手を尽くしても、AIによるレビューであらゆるバグを摘出できるわけではありません。 しかしここではいったんバグの摘出を主目的から外して、テストの役割を再考してみます。

開発をサポートする自動テスト

バグ摘出のためではなく、開発を円滑に進めるためにユニットテストや統合テストを活用します。

  • 完了の定義として:テストがパスしていることで「仕様を満たしていると判断できる」のであれば、AIエージェントに与えるゴールとして使えます(テストファースト*6)
  • リグレッションテストとして:追加されたテストは、以降の変更によるリグレッション(退行、デグレード)から保護してくれます

テストコード(スクリプト)による自動テストはLLMと異なり、決定論的に動作するよう設計できます*7。 仕様から導いた振る舞いを同じ基準で繰り返し検証できることは、イテレーティブな開発スタイルにおいて大変役に立ちます。

以下、補足です。

仕様のテストを書く

重要なのは、仕様をもとにテストコードを書くことです。 すでにあるコードをもとに実装のテストを書いてしまうのは、AI以前からユニットテストのアンチパターンです*8。 実装の誤りをそのまま正解とするテストになるだけでなく、実装の詳細に依存したテストは壊れやすく、リグレッションテストとしてのROIも下がります。

壊れやすいテストが増えてくると、以前はメンテ不能になって放棄されていたものですが、今はAIがトークンを無駄に消費してメンテしてくれます。 これは意識していないと気付きにくい問題です。

また『Investigating Test Overfitting on SWE-bench』という論文では、LLMの生成した不完全なテストを使ってコードを反復修正していくと、そのテストにはパスするものの、実際には問題を解決できていないパッチを生成してしまうケースが示されています。 AIエージェントにとって、テストがパスすることは明確なゴールになります。だからこそ、与えるテストが正しい仕様を表現しているかが重要です。

テスト技法を活用する

仕様からテストケースを作成するにはテスト技法(テスト設計技法)が役立ちます。 プロダクトコードの実装前に設計が必要なように、テストケースもテスト設計を経て作ります*9。

また、テスト技法によるモデリングには仕様の曖昧さや考慮漏れをあぶり出す効果もあり、「欠陥の作りこみを防ぐ」につながります*10。

テスト技法を学びたいかたにはこちらの書籍がお勧めです。

UIのテストも書く

UI(レイアウトや操作)のテストは、テストコードの作成や保守にコストがかかる割に効果を得にくいため、目視確認に頼っていた領域も多いでしょう。 しかし、AIコーディングエージェントに与えるゴールとして考えると、自動化する価値が高まります。 押せるべきボタンが画面外にあったり、他のコンポーネントに隠れていたりして押せないなどは、テストコードで機械的に検出できると捗ります *11 *12。

統合テストに位置づけて実施しており、ユニットテストで検証された部品同士を統合(結合)して機能することを検証するのが目的です。 網羅的な検証は実行の早いユニットテストに任せ、UIテストでは主要な操作経路とコンポーネント間の連携を確認します。

テストコードの保守性の問題

たとえ仕様をもとにテストを書かせても、AIエージェントはテストコードを書きすぎる傾向があります。 またそもそも保守性の低いコードを書きがちで、テストコードとしては看過できない特性です。

具体例として、Slay the SpireクローンをすべてAIエージェントに作らせたときのcode : test ratioは1 : 5.0でした。 テストコード量に適正な比率があるわけではありませんが、私が普段めやすとしている1 : 1.0〜2.0と比べるとかなり多く、統合テストを含むとはいえもっと減らしたいと考えています。

保守性についてはスキルや静的解析(アナライザーやLint)である程度は改善できていますが、まだ試行錯誤しているところです。

QA担当者によるテスト

開発者テストで仕様どおりに動作することまで確認できていれば、QA担当者によるシステムテストはより高い目線で実施できます。 見た目・手触り感、また大局的な観点などは仕様書やコードのレビューでは十分に評価できないため、そういった問題の摘出も期待されます。

もちろん、QA担当者が開発者とは異なる観点で仕様書ベースのテストを実施する価値はあります。 この手のテストは、継続的に繰り返し実行するものであればスクリプトによる自動化を検討すべきですが、1〜2回しか実施しないのであれば手動テストやAIエージェントに操作させてもよいでしょう*13。 ROIを考えて選択しましょう。

必要なのはテスト戦略

万能なテスト手段というものはありません。目的とする品質を定め、複数のアプローチを組み合わせてカバーする*14「テスト戦略(test strategy)」を立てることが必要です。 テスト戦略は、開発・QAで個別ではなく、プロジェクト全体で考えましょう。

また、あえてやらないテストを決めるのも戦略です。 AIによってテストコードの生成コストは下がりましたが、だからといってテストを増やすことが最適とは限りません。 一方、AIによるレビューなど、従来より低コストで利用できる検証手段も増えています。何を確認したいのかを明確にし、目的に応じて手段を選択することが重要です。

第5戒──自動化しろ。これは最終段階だ。ネバダでもフリーモントでも、一番のまちがいは、ステップの自動化から始めてしまったことだ。要件をすべて洗い直し、部品や工程を減らせるだけ減らし、バグをつぶし切るまで自動化は待たなければならない。

そう、イーロン・マスクも言っていました*15。

参考

自動テストの特性については、こちらの8原則も参照してください。

sites.google.com

こちらの同人誌も参考になるはずです。

www.nowsprinting.com

Unityにおけるユニットテストの書き方について書いた同人誌です。テスト技法についての章もありますので、Unityで開発しているのであればまずこちらから入ることをお勧めします。

www.nowsprinting.com

UnityにおけるAIコーディングエージェントを使ったテストファースト開発、テスト設計を含むスキルを公開しています。

www.nowsprinting.com

*1:今後支援先で「これ読んで」する用

*2:最新版ではより広い視点で9項目が挙げられていますが、テストの目的がシンプルに表現されている2011年版から引用しました

*3:広義のテスト活動の意で、ドキュメントレビューや静的解析も含む

*4:「シフトレフト」は2001年に提唱された言葉ですが、テストを開発の早い段階から実施するという考え方はそれ以前から存在していました(「Wモデル」など)

*5:レビューで確認すべき観点を指示する際は、同値分割法、境界値分析、状態遷移テスト、エラー推測などのテスト技法(後述)が役立ちます。なお、AIは言葉は知っていても実践できるとは限らないので、噛み砕いて伝えるようにしてください

*6:工程で考えると 仕様→実装→テスト なので、テストを先に書くことは逆転しているように見えますが、抽象度で考えれば 仕様→テスト→実装 は自然です

*7:スクリプトの書きかたによっては不安定なフレーキーテストになるので注意

*8:実装から仕様を抽出して仕様のテストを書かせられれば話は変わってきますが、難しそう。私は必要でないので取り組んでいません

*9:慣れると脳内でやってしまいますが

*10:形式手法も活用できますが、本稿では割愛します

*11:私はビュー(MVCのView)相当をAIエージェントに書かせているため、レイアウト上の重なり・はみ出しなども機械的に検出させています。完全なレイアウトを検証するテストは壊れやすいため非推奨です

*12:レイアウトは、スクリーンショットを画像解析して検証させることも効果的です。ただしテスト実行のたび行なうとコストが高くなるため、実行契機は絞るとよいでしょう

*13:LLMの推論によるテスト実行は、スクリプトベースの自動テストよりも手動テストに近い特性をもちます。「自動化」でひとくくりにしないことをお勧めします

*14:複数のレイヤーを重ねる考え方は「スイスチーズモデル」や「DevOpsバグフィルター」として知られています

*15:引用は生産ラインの話なので「バグをつぶし切る」は本稿にはそぐわないのですが、原文のまま

AIコーディングエージェントが踏みがちな Unity Test Framework の落とし穴を実行前に検知する UTF Analyzers を公開しました

Unity Test Frameworkパッケージには、Unityエディターをフリーズさせてしまうような落とし穴が依然として存在します。 拙著『Unity Test Framework完全攻略ガイド』を読んでいれば回避できるものですが、同人誌を読んでいないAIコーディングエージェントは容易にその穴にはまります*1。

そこで、これらをテスト実行前に検知するRoslynアナライザー「UTF Analyzers」を作成し、OSSとして公開しました。

github.com

一般にLLMは否定形の命令に弱く、Agent Skillsなどによる回避は確実性に欠けるうえ、コンテキストウィンドウを無駄に使います。 またUnityエディターがフリーズしたとき、その原因がテストコードの書き方によるものとは考えないようで(それはそう)、環境要因を疑って迷走しはじめます。 そのため、コンパイル時にアナライザーによって防ぐのが最適解です。

もちろん人間もうっかりミスをするので、自らテストコードを書く際にも有効です。 現状はクリティカルな問題を回避するアナライザーと、他のアナライザーの診断のうちテストコードにそぐわないものを抑止するサプレッサーのみ実装していますが、今後、よりよいテストコードの書き方をサジェストする診断も追加していく予定です。

なお、主用途がAIコーディングエージェント向けのため、CodeFixは提供していません。

診断ルール(v1.1.0 時点)

構造 (UTF1xxx)

テストメソッドのシグネチャおよび属性の組み合わせに関するルール。 UTFのバージョンによりますが、Unityエディタがフリーズしたり、実行時エラーになるもの。デフォルトの重大度はErrorです。

Id Title
UTF1001 コルーチン形式のテストメソッドでは、TestCase 属性および TestCaseSource 属性はサポートされません
UTF1002 テストメソッドの戻り値の型として Task<TResult> はサポートされません
UTF1003 コルーチン形式のテストメソッドでは、Pairwise 属性、Sequential 属性、および Combinatorial 属性はサポートされません
UTF1004 async メソッドでは、OneTimeSetUp 属性および OneTimeTearDown 属性はサポートされません
UTF1005 ICommandWrapper を実装する属性は、async テストメソッドおよびコルーチン形式のテストメソッドではサポートされません

アサーション (UTF2xxx)

アサーションおよび制約の誤用に関するルール。 Unityエディタがフリーズしたり、実行時エラーになるもの。デフォルトの重大度はErrorです。

Id Title
UTF2001 Assert.ThrowsAsync、CatchAsync、および DoesNotThrowAsync はサポートされません
UTF2002 Throws 制約、Assert.Throws、Assert.Catch、および Assert.DoesNotThrow の実際の値として、async デリゲートはサポートされません
UTF2003 制約モデルの実際の値として、async デリゲートはサポートされません
UTF2004 AllocatingGCMemory 制約の実際の値としてサポートされるのは TestDelegate のみです
UTF2005 DelayedConstraint はサポートされません

サプレッサー (UTF3xxx)

テストコードにそぐわない、他のアナライザーによって報告される診断を抑制します。

Id Title
UTF3001 Assert.Multiple が利用できない場合、NUnit2045 (Use Assert.Multiple) を抑制します
UTF3002 メンバーが UnitySetUp または UnityOneTimeSetUp メソッドで初期化される場合、CS8618 (Non-nullable field or property is uninitialized) を抑制します
UTF3003 UnityTearDown または UnityOneTimeTearDown メソッドを持つテストフィクスチャでは、CA1001 (Types that own disposable fields should be disposable) を抑制します
UTF3004 UnitySetUp、UnityOneTimeSetUp、UnityTearDown、および UnityOneTimeTearDown メソッドでは、NUnit1028 (The non-test method is public) を抑制します
UTF3005 テストメソッドでは、CA1707 (Identifiers should not contain underscores) を抑制します
UTF3006 テストメソッドでは、VSTHRD200 (Use Async suffix for async methods) を抑制します

拡張 (UTF5xxx)

カスタム属性、制約、および比較器の作成者向けのルール。 UTFのバージョンによりますが、Unityエディタがフリーズしたり、実行時エラーになるもの。デフォルトの重大度はErrorです。

Id Title
UTF5001 ApplyToTest、ApplyToContext、および Wrap は例外をスローしてはいけません

使用方法

UPMパッケージ版は、パッケージをインストールするだけでプロジェクト内のすべてのテストアセンブリにアナライザが適用されます*2。 またNUnit.Analyzers v3.9.0も同梱しています。

openupm.com

UTF Analyzers単体はNuGetパッケージとしても配布しており、NuGetForUnity および UnityNuGet*3からインストールできます。

www.nuget.org

諸事情あってパッケージIDが表記揺れを起こしていますが見逃してください。

参考

ユニットテストを体系的に学びたい、AIコーディングエージェントの書いたテストをちゃんとレビューしたい、フレームワークを拡張したい、という方はぜひお読みください! www.nowsprinting.com

AIコーディングエージェントにテスト設計とテストファースト開発をさせるAgent Skillsも公開しています。 www.nowsprinting.com

*1:NUnitの知識で書くとはまります

*2:asmrefを利用して、UnityEngine.TestRunnerアセンブリに入っている状態になっています

*3:執筆時点ではリリース待ち

Test Helper v1.6 : UIレイアウト系のカスタム制約

Unity Test Frameworkによるテストを書くときに便利な Test Helper パッケージの v1.6 をリリースしました。 本バージョンでは、UIレイアウトを検証するための4つのカスタム制約(constraint)を追加しています。

github.com

UIレイアウトは変更される可能性が高いため、テストを書く投資対効果(ROI)が低く「書くべきでない」領域でした。 しかし、昨今、AIコーディングエージェントにsceneやprefabを編集させるようになると、人間では起こさないようなミス(コンポーネントの重なり、はみ出しなど)が発生します。 これを自律的かつ低コストで修正させるため、最低限の検証を機械的に行うために使用します。

WithinScreen制約

WithinScreen制約は、モーダルダイアログなどのUI要素が画面内に表示されている(はみ出ていない)ことを検証します。

次のように使用します。

using Is = TestHelper.Constraints.Is;

[TestFixture]
public class MyTestClass
{
    [Test]
    public void MyTestMethod()
    {
        var confirmDialog = GameObject.Find("ConfirmDialog");

        Assert.That(confirmDialog, Is.WithinScreen);
    }
}

失敗した場合は、次のように具体的なメッセージが出力され、コーディングエージェントに修正を促します。

Expected: RectTransform within screen (0, 0, 640, 480)
 But was: "ConfirmDialog" (-12.0, 437.0, 50.0, 50.0) exceeds the left edge by 12.0px and the top edge by 7.0px

なお、検証したいUI要素が複数あるときは、AllオペレーターでAssert.That(elements, Is.All.WithinScreen);と書けます。

また、修飾子.Within(float)で許容誤差を指定できます。デフォルトは 0.5f です。

WithinContainer制約

WithinContainer制約は、UI要素がダイアログなどのコンテナ要素内に表示されている(はみ出ていない)ことを検証します。

次のように使用します。

using Is = TestHelper.Constraints.Is;

[TestFixture]
public class MyTestClass
{
    [Test]
    public void MyTestMethod()
    {
        var viewport = GameObject.Find("Viewport").GetComponent<RectTransform>();
        var card = GameObject.Find("Card (0)");

        Assert.That(card, Is.WithinContainer(viewport));
    }
}

修飾子.Horizontally()もしくは.Vertically()で、横方向・縦方向のみ検証できます。 また、.Within(float)で許容誤差を指定できます。デフォルトは 0.5f です。

Overlapping制約

Overlapping制約は、UI要素同士の重なりを検証します。

次のようにNotオペレーターで「重なっていない」ことを検証できます。

using Is = TestHelper.Constraints.Is;

[TestFixture]
public class MyTestClass
{
    [Test]
    public void MyTestMethod()
    {
        var cards = GameObject.Find("CardGrid").GetComponentsInChildren<Image>();

        Assert.That(cards, Is.Not.Overlapping);
    }
}

修飾子.Ignoring(group)で、group間の重なりを検証から除外できます。 また、.Within(float)で許容誤差を指定できます。デフォルトは 0.5f です。

TextOverflowing制約

TextOverflowing制約は、TextおよびTMP_Textのテキストのはみ出し/切り詰めを検証します。

次のようにNotオペレーターで「はみ出し/切り詰めされていない」ことを検証できます。

using Is = TestHelper.Constraints.Is;

[TestFixture]
public class MyTestClass
{
    [Test]
    public void MyTestMethod()
    {
        var flavorText = GameObject.Find("Flavor Text");

        Assert.That(flavorText, Is.Not.TextOverflowing);
    }
}

また、修飾子.Within(float)で許容誤差を指定できます。デフォルトは 0.5f です。

補足:アサーションでやらないこと

今回追加した制約で検証するのは、恒久的に変更されないであろう(テストが壊れにくいであろう)観点のみです。 このほか、たとえばCanvasScalerの設定などプロジェクトでルールがあるものは、scene/ prefabのバリデーションを行なうテストを書くとよいでしょう。

筆者が公開しているunity-coding-skillsでは、機械的なアサーションをパスした後、コンポーネントの位置関係、色(コントラスト)、タイポグラフィなどの観点について、画像認識で検証させています*1。

github.com

参考

カスタム制約、scene/ prefabのバリデーション方法などは、こちらで解説しています。

www.nowsprinting.com

*1:CIなどで毎回実行するのではなく、セッション内で変更があったときのみ実行

BannedApiAnalyzers.Unity を公開しました

Microsoft.CodeAnalysis.BannedApiAnalyzers は、プロンプトで使用を禁止したいAPIを設定ファイルに書くことでコンパイル時に診断できるRoslynアナライザです。

もちろんUnityでも使用できますが、Unityでは自動生成される .csproj ファイルに設定ファイルのパスを追加する必要があります。 これはエディタスクリプトで .csproj ファイル生成をフックして書き換えれば実現できますが、少し面倒です。

このエディタスクリプト実装が不要になるようフォークして変更した BannedApiAnalyzers.Unity を作ったので紹介します。

github.com

nugte.org にも公開済みです。

www.nuget.org

BannedApiAnalyzers.Unity の利点

.csproj ファイルの書き換えが不要

素の Microsoft.CodeAnalysis.BannedApiAnalyzers では、設定ファイルとして BannedSymbols.txt もしくは BannedSymbols.\*.txt を要求します。 ファイル名は固定ですが、ファイルパスを .csproj ファイルに AdditionalFiles として追加しなければなりません。

一方、Unityでは2021.3から、特定のファイルパスを .csproj ファイルに AdditionalFiles として追加する機能が使えます*1。 しかしファイル名に制約があり、Filename.[Analyzer Name].additionalfile フォーマットでないと処理されません。つまり、BannedSymbols.txt というファイルをアナライザに渡す手段はありません。

BannedApiAnalyzers.Unity は、UnityのAdditional files機能で渡されたファイルを設定ファイルとして読みます。 つまり、プロジェクトの Assets/ 下に <Filename>.BannedApiAnalyzers.Unity.additionalfile という名前のファイルを置くだけで、禁止するAPIを指定できます。

なお、次の点に注意してください。

  • <Filename> 部分は必須で、かつ途中にピリオドを含んではいけません
  • Assets/ 直下だけでなくサブディレクトリのどこに置いたファイルでも、プロジェクト全体に適用されます。ディレクトリやアセンブリごとに設定を使い分けることはできません
  • Packages/ 下にある組み込みパッケージにも適用されます

v3.11 ベース

nuget.orgに公開されている Microsoft.CodeAnalysis.BannedApiAnalyzers は v3.3.4 です。 次バージョンである v3.11 は安定版をリリースされないまま dotnet/roslyn に統合され、パッケージとしての公開はされていません。

BannedApiAnalyzers.Unity は、v3.11 のソースをベースにしています。v3.3.4 との差異は次のとおりです(Claude Code調べ)。

新機能(v3.11.0 で追加)
機能 概要
名前空間の ban 対応 BannedSymbols.txt に N:SomeNamespace と書くと、そのネームスペース内のすべての型参照に RS0030 が発火する。v3.3.4 では未対応(エントリが無視された)。
継承元の ban 検出 class Derived : BannedBase {} のような継承構文に対しても RS0030 が発火するようになった。Base-type syntax node への RegisterSyntaxNodeAction が追加。
// コメントのサポート BannedSymbols.txt で行末に // 説明 と書けるようになった。v3.3.4 では // 以降が DocumentationCommentId の一部として解釈され、シンボル解決が静かに失敗していた。
内部変更(挙動に影響する可能性あり)
変更 影響
シンボル解決の遅延化 解決を Lazy<ImmutableArray<ISymbol>> に変更。パフォーマンス改善で機能差異はほぼないが、曖昧な ID のエッジケースで挙動が変わる可能性あり。
重複検出ロジック変更 v3.3.4: 解決済み ISymbol オブジェクトの比較。v3.11.0: 生の DeclarationId 文字列(trim 後)で grouping。未解決シンボルでも重複が検出されるように。

使いかた

インストール

アナライザのインストールは、NuGetForUnity か UnityNuGet (OpenUPM)*2 からがおすすめです。

NuGetForUnity の場合、次の手順でインストールすると、全アセンブリに適用されます。

  1. Open the NuGetForUnity window via NuGet > Manage NuGet Packages
  2. Search "BannedApiAnalyzers.Unity" and click Install

UnityNuGet の場合、openupmコマンドかPackage Managerウィンドウでインストールしてから、適用させたいアセンブリの Assembly Definition References に BannedApiAnalyzers.Unity_Unity を追加します。

openupm add org.nuget.BannedApiAnalyzers.Unity

NuGetForUnity と UnityNuGet の違いについて詳しくは、次の記事を参照してください。

www.nowsprinting.com

設定ファイル

Assets/ 下の任意のディレクトリに設定ファイルを作ります。ファイル名はフォーマット <Filename>.BannedApiAnalyzers.Unity.additionalfile を守る必要があります。

設定ファイルにはDocumentation Comment ID形式で禁止したいAPIを列挙します。書式は ID string format を参照してください。

たとえば、Assets/BannedSymbols.BannedApiAnalyzers.Unity.additionalfile に次のように書きます。

M:UnityEngine.GameObject.Find(System.String)

すると、次のように GameObject.Find(string) メソッドの使用箇所で警告が出ます。

なお、Documentation Comment ID の後ろに ; を挟んでメッセージを書けるので、どう書き換えるべきかを書いておくとよいでしょう。特にAI Coding Agentを使用するなら必須といえます。

補足

重大度の変更

デフォルトでは、BannedApiAnalyzers.Unity による診断の重大度(severity)は Warning です。 これは .ruleset、.globalconfig、もしくは .editorconfig ファイルで変更できます。詳しくは、それぞれ過去記事を参照してください。

www.nowsprinting.com

www.nowsprinting.com

サポートUnityバージョン

BannedApiAnalyzers.Unityが動作するのは、Unity 2021.2 以降です。 Additional files 機能が(おそらく)Unity 2021.3 からなので、それに合わせてあります。

*1:https://docs.unity3d.com/Manual/roslyn-analyzers-additional-files.html

*2:執筆時点ではOpenUPMはリリース待ち

Unityプロジェクトでテスト設計+テストファースト開発のための Agent Skills

昨年、Unityプロジェクトでも Claude Code に自走させるワークフロー という記事を投稿しました。 その後も .claude/ 下を公開したリポジトリのメンテナンスを継続していましたが、改めて Agent Skills/ Claude Code プラグインとして公開しました。

github.com

実装計画スキル

代表スキルは /plan-feature <SPEC> で、これをプランモードで使用すると実装前にテスト設計を行い、プランファイルにテストケースを含めてくれます。また実装をテストファースト(TDDではなく)で進めるワークフローもプランファイルに含めます。

このスキルおよびワークフローには、次の特徴があります。

  • テスト設計
    • 保守性の高いテスト:AIが書きがちな、冗長なテスト、アサーションのないテスト、不要なテストダブル(モック/ スタブなど)を削減
    • 手動検証は最低限:統合テストレイヤでUI操作を含めたテストを実施、目視検証が必要なテストは画像解析による検証を実施*1。アニメーションや手触り感などの観点のみ人間に評価を移譲
    • プラン時点でテストケースをレビュー可能:検証内容も含めてプランファイルに出力されるので、テストケースをレビューすることでプランが仕様を満たしているかどうか判断できる
  • テストファースト
    • テストコードを先に実装・実行して、テストが失敗することを確認します。テストファーストには次の利点があります
      • テストコードの正当性(少なくとも、ちゃんと失敗できるテストである)が担保される
      • プロダクトコード(ゲーム本体コード)実装の完了基準を定義できる
      • テスト駆動開発(TDD)と比べ、変更ループが少なくトークン消費も抑えられる
  • コーディングガイドライン
    • Unity Test Framework向けテスト実装についてのガイドライン(非同期テストは UnityTest 属性でなく async + Test 属性を使う、など)
    • Unity向けコード実装についての最低限のガイドライン(Unity 2021.1以降ではオブジェクトプーリングを自前実装しないで ObjectPool<T> を使う、など)
    • Scene/ Prefab の編集は、エディタスクリプトをアドホックに書いてUnityエディタで実行
    • ScriptableObject などの複雑でないアセットファイルを直接編集するガイドライン
  • 内部品質(保守性・可読性など)
    • テスト可能なコードであるという点で、最低限の内部品質は確保
    • ワークフローの最後に、決定論的ツールによる静的解析を実行(後述)

インストールと設定

プラグイン/ スキルのインストール

Claude Code プラグインとしてプロジェクトスコープにインストールする場合は、次のスラッシュコマンドを実行します。

/plugin marketplace add nowsprinting/unity-coding-skills
/plugin install unity-coding-skills@nowsprinting-unity-coding-skills --scope project

その他、スキル単位でインストールするツールなどが使えるはずです。

MCPサーバの設定

一部のスキルは、JetBrains RiderビルトインのMCPサーバ及び、Unityエディタを操作する拡張を使用します。 設定方法は次の記事を参照してください。 なお、設定ファイルを手書きする場合は、MCPサーバの名称は必ず jetbrains にしてください。

www.nowsprinting.com

Riderを使いたくない場合、一部のスキルだけ書き換えてもらえれば動作はするはずです。 しかし、JetBrains MCPサーバにはファイル検索などCoding Agentを効率化するツールも含まれていますので、Riderの使用をお勧めします。 無料トライアルもあります。

静的解析の設定

強制したいコーディングルールは、コンパイラやRoslynアナライザで warning 以上の診断になるように設定しておくと、ワークフローの最終フェーズで修正されます。

たとえば、(AIがやりがちな)変更で使わなくなった型やメンバを削除せず放置するのを禁止するには、次の設定を .editorconfig ファイルに追加します。

resharper_unused_type_local_highlighting = warning
resharper_unused_type_global_highlighting = warning
resharper_unused_member_global_highlighting = warning
resharper_unused_member_local_highlighting = warning

.editorconfig ファイルについて詳しくは次の記事を参照してください。

www.nowsprinting.com

またRiderを使用できる場合、プラグインによる診断も対象になります。 (これもAIが書きがちな)長く複雑なメソッドを分割させるために、サイクロマティック/ コグニティブ複雑度を診断するプラグインが利用できます。 詳しくは次の記事を参照してください。

www.nowsprinting.com

注意事項

  • ドキュメントに "TBD" と書いてある仕様は無視されます(詳細を詰められたりしない)
  • Scene/ Prefab 編集スクリプトは、コミットも削除もしません(中身を見たいことがあるため)
  • スキルにはコーディング規約を含まないため、たとえば割とカジュアルにリフレクションが使われたりします。まず静的解析で縛ることを考え、決定論的に書けないものはプロジェクト固有のガイドラインを書くなどしてください
  • 構造的な問題や落とし穴などの観点は人間によるコードレビューが必要です
  • コード品質は既存のコードベースに引っ張られます。既存プロジェクトに導入する場合は、まず /refine-tests スキルでテストコードをリファインすることをお勧めします

使用感・メトリクス

主に、Slay the Spireクローンの制作に使用したときのもの。

  • プルリクエストにするくらいの粒度で使用して、プラン承認後1〜2時間(最近遅いので)自走して、実装・テストを完了します
    • UIレイアウトは雑なものですが、ちゃんと操作できるものが出来上がります(他のGameObjectによってブロックされていないことなどもテスト済み)
    • バグは出ますが仕様定義の漏れが支配的で、仕様レビューを厳密にすることで防げるはず(本プラグイン対象外)
  • テストケースレビューだけで、コードレビューは(テストコードもプロダクトコードも)省略することがほとんど*2。テストケースレビューも全ケースではなく、統合テストや受け入れテスト*3を中心にレビューしています
  • 内部品質は十分とは言えませんが、Claude Codeで開発イテレーションを繰り返しても崩壊しない程度の品質はキープできています*4
  • ステートメントカバレッジは 97.5% *5
  • code : test ratio は 1 : 5.0 *6

参考

本プラグインのスキルがベースとしているテスト設計・実装、メトリクス、Roslynアナライザなどについて詳しくは、次の同人誌を参照してください。 テストケースをレビューする際の観点・判断基準や、スキルをカスタマイズする助けになるはずです。

www.nowsprinting.com

*1:スクリーンショットを撮るテストコードに検証観点を残しているので、目視検証を含めたリグレッションテストを指示すれば再実行も可能。将来的に初回パスしたら以降は正解画像との画像比較によるビジュアルリグレッションテストに変換する案もあり

*2:スキル改善のために見ることはあります

*3:まだ精度が低いのですが、要求に対応したテストに「受け入れテスト」マーカーがつきます

*4:仕事で書くライブラリなどのコードは、ここから温かみのある手作業でリファクタリングしています

*5:ゲームの特性と、I/Oや通信がなく例外処理がほぼないためカバレッジは高め

*6:AIはテストを書きすぎる傾向があるので、この code : test ratio を低く抑えることが直近の課題

Gameplay MCP Server for Unity を公開しました

Unity製ゲームのランタイムに組み込んでAIエージェントにゲームをプレイさせるためのMCPサーバパッケージを公開しました。 Unityエディタを操作するMCPサーバは多数公開されていますが、これは操作対象がゲーム本体で、エディタ内だけでなくプレイヤービルドでも動作するものです*1。

基本的なツールの実装を終えた状態で、バージョンは 0.3.0 です*2。

github.com

MCPサーバ部分には MCP C# SDK を採用、ゲームタイトルで自由にカスタムツールを追加できます。 uGUI要素の操作は DeNA/Anjin でも使用されている UI Test Helper を使用しており、GameObjectの出現を(固定時間でなく)ポーリングで待ったり、ユーザの操作をブロックするオブジェクトがあるときは操作できないと判断するなど、UIテストに必要な機能を備えています。また、ゲームタイトル固有のカスタムUIフレームワークにも対応できます。

ビルトインツール

ビルトインで提供しているツールは次の5つです。

list_available_actions

画面に表示されている操作可能な*3 GameObject と、可能な操作(クリック、ドラッグなど)の組み合わせを返します。 モデルはこの中から次のアクションを選択できます。

invoke_action

対象 GameObject と操作を指定して、操作を実行します。

inspect_game_object

GameObject を、name, path*4, text*5, texture*6 を指定して検索し、プロパティを取得します。 表示まで時間がかかる場合でも GameObject の出現を一定時間ポーリングして待ちます。 操作の結果、表示されるはずの GameObject を確認するのに使用できます。

take_screenshot

スクリーンショットを撮影して返します*7。

list_scenes

ロードされているSceneを返します。簡易的なゲームの状態確認ツールです。

カスタムツールの実装

ゲームタイトル固有のカスタムツールを追加できます。たとえば次のような用途を想定しています。

  • 現在のゲームの状態を返す
  • uGUI以外の操作
  • デバッグコマンドの実行

カスタムツールは、次のように MCP C# SDK の [McpServerToolType] および [McpServerTool] 属性を配置することで認識されます。

// Assets/Scripts/Runtime/MyGameTools.cs (custom tools created by the game title)
// Just add [McpServerToolType] and it's registered automatically!
[McpServerToolType]
public class MyGameTools
{
    [McpServerTool(Name = "get_player_status", ReadOnly = true, Destructive = false)]
    [Description("Returns the player's current status as JSON.")]
    public async Task<string> GetPlayerStatus(CancellationToken ct = default)
    {
        await UniTask.SwitchToMainThread(ct);
        var player = GameObject.FindWithTag("Player");
        return JsonSerializer.Serialize(new { hp = player.GetComponent<Health>().Current });
    }
}

なお、たとえばゲームの状態を返すツールを実装したことによって list_scenes ツールが不要になったときは、次のように無効化できます。

var config = new McpConfig();
config.DisabledTools.Add("mygame.list_scenes");

使用方法

インストール

あらかじめ MCP C# SDK v1.0.0 以降をインストールします。 NuGetパッケージなので NuGetForUnity などを利用してください*8。NuGetForUnityの使いかたについては過去記事を参照してください。

www.nowsprinting.com

続いて Gameplay MCP Server for Unity パッケージをOpenUPMからインストールします*9。

  1. Project Settings ウィンドウを Editor > Project Settings で開き、Package Manager タブを選択
  2. Scoped Registries 下の + ボタンをクリックして次のように入力します
    1. Name: package.openupm.com
    2. URL: https://package.openupm.com
    3. Scope(s): com.nowsprinting and com.cysharp
  3. Package Manager ウィンドウを Window > Package Manager で開き、My Registries タブを選択
  4. Gameplay MCP を選択して Install ボタンをクリック

MCPサーバの起動

MCPサーバは次のコードで起動できます。 [RuntimeInitializeOnLoad] 属性などでゲーム起動時に常に起動したり、デバッグメニューから起動するなどします。

var config = new McpConfig
{
    OperatorPool = new OperatorPool()
        .Register<UguiClickOperator>()
        .Register<UguiDragAndDropOperator>()
        .Register<UguiTextInputOperator>()
};
var server = new McpServer(config);
server.StartAsync().Forget();

McpConfigには使用するオペレーターのほか、GameObjectを探す GameObjectFinder、操作可能の判断に使用するストラテジ関数などを指定できます。 これらはUI Test Helperパッケージの機能なので、詳細はUI Test Helperのドキュメントを見てください。

なお、サーバの待ち受けポートは McpConfig のほか、コマンドライン引数でも指定できます。マルチプレイヤーでも安心。

AIエージェントの設定

次のように設定します。Claude Codeの例:

{
  "mcpServers": {
    "gameplay": {
      "type": "http",
      "url": "http://localhost:8010/mcp"
    }
  }
}

Agent Skills

MCPサーバだけでも操作手順を指示する形であれば動くはずですが、ゲームを自律的プレイさせるには、ゲームのルール、目的などをAgent Skillsとして与える必要があります。

また、uGUIの操作ができなかったときなどの問題の切り分けのために、UI Test Helperのトラブルシュート資料もスキル化しておくと役立つはずです。

今後の展望

あといくつかツールを追加*10するほか、UI Test Helperの Paginator 関連の機能追加などを予定しています。

*1:IL2CPPビルドでも動作しますが、仕組み上WebGLは対象外です

*2:中途半端ですが、なんとなく今週アナウンスしようという気持ちになたっため

*3:interactable=true かつ、カメラからレイキャストが通る(ほかのオブジェクトにブロックされていない)もの

*4:ヒエラルキーのパス

*5:ボタンなどの表示テキスト

*6:ボタンなどのテクスチャファイル名

*7:スクリーンショットは補助的な、結果確認用ツールの位置づけなのですが、エージェントは割と安易に使ってきます。無駄なトークン消費を抑えるためには、Agent Skillsで用途を限るように書くなどして抑制してください

*8:UnityNuGetには登録済みで、レジストラへの反映待ち

*9:git urlでもインストールできますが、その場合は依存パッケージを手動でインストールしてください

*10:persistentDataPathアクセスなどを予定

MCP Server Extension for Unity v1.0.0 リリース

Unity Editor 向けの新しい MCPサーバ、MCP Server Extension for Unity をリリースしました。 JetBrains Riderにインストールするプラグインで、RiderにビルトインされているMCP Serverにツールを追加する拡張機能です*1。

特徴は次の3点です。

  • UnityプロジェクトへのUPMパッケージインストールなし*2で利用可能
  • ビルトインの MCP Server を設定済みであれば、追加設定なしで利用可能
  • 複製したワークスペース*3からの同時使用も追加設定なしで可能

提供するツールは次の4つ。MCP Serverは21のツールを提供していますが、それに+4されます。

  • run_unity_tests: Unity Test Runnerでテストを実行します
  • run_method_in_unity: 任意のstaticメソッドを実行します*4。このツールで、sceneおよびprefabファイルを編集できます
  • get_unity_compilation_result: アセットをリフレッシュし、コンパイル結果を取得します
  • unity_play_control: 再生モードを操作します

プラグインページはこちら。

plugins.jetbrains.com

リポジトリはこちら。

github.com

利用方法

プラグインのインストール

  1. Settings > Plugins を開く
  2. Marketplace タブで "mcp unity" などで検索
  3. Install をクリック

MCPサーバの設定

ビルトインの MCP Server を設定済みであれば、すぐ使えます。

未設定の場合は

  1. Settings > Tools > MCP Server を開く
  2. Enable MCP Server をクリック
  3. お使いのCoding Agentに応じた Auto-Configure をクリック

詳しくは MCP Server ドキュメントを参照してください。

Agent Skills

必須ではありませんが、テスト実行ツールで指定するテストモードやアセンブリ名の求め方を指示するAgent Skillを設定すると便利です。 リポジトリのREADMEにサンプルを掲載していますので、参考にしてください。

アーキテクチャ

JetBrains IDEs 2025.2以降でビルトインされているMCPサーバには、カスタムプラグインでツールを拡張できる仕組みがあります。 Coding Agentとのインタフェースはこれを利用しています。個別に設定する手間を省けるほか、複数ワークスペースからの同時使用も考慮されており、ツールの実装に集中できます。

Rider向けプラグインは、JetBrains IDEsプラグインとして振る舞うためのKotlinフロントエンド層と、C#バックエンド層の2レイヤ構成です。 本プラグインで提供しているツールは、RiderのUnityプラグインが備えている(開かれている)インタフェースを呼び出し、Unityプラグインを経由してUnity Editor側のRider Editorパッケージと通信し、Unity Editorを操作しています。

この構造のため、Rider Editorパッケージの機能の範囲でしかツールを提供できません。従って今後機能追加の予定はなく、バグフィックス中心のメンテナンスになります。 また近い将来、JetBrains公式で同じ機能が提供されるはずです。そうなれば、このプラグインの公開も終了するつもりです。IDEバージョンの追随がめんどくさいので早く手放したい(本音)。

所感

コードはほぼClaude Codeさんが書きました。メンテしない前提なので内部品質は気にしない、ということで、狭義のvibe coding寄りで。 でもテストは書かせているしテストケースをレビューしているので、vibe codingとは言えない。普段の自分からはだいぶ寄っているという程度です。

Plan modeでテスト設計させたスキル*5やPlanファイルもリポジトリに入れてあるので、雰囲気わかっていただけるかと。

関連記事

www.nowsprinting.com

www.nowsprinting.com

*1:従って、正しくは「MCPサーバ」そのものは提供していません

*2:ビルトインのRider Editorパッケージは使用します

*3:cp, git clone, git worktree, claude --worktree などによる

*4:引数なしのメソッドのみで、戻り値は受け取れません

*5:Unity向けの移植なのでだいぶ適当