NOTE · 2026-05-09
Unity Domain 層を dotnet 姉弟 .csproj で CI に乗せる移植 playbook (30 分)
Unity Personal license の CI activation が死んだので、Domain 層を Unity ランタイム抜きで dotnet test に流す姉弟 .csproj パターンを別プロジェクトに移植するための手順書。前提診断 3 項目 + 7 ステップチェックリスト + つまずき 5 個。条件が揃っていれば 30 分から 1 時間で乗る。
前回の記事 で AtmoTest プロジェクトに導入した「Unity Domain 層を dotnet test で CI に乗せる」姉弟 .csproj パターンを、他の Unity プロジェクトに 再利用可能な playbook として独立に整理する。なぜこの戦略を選ぶに至ったかは前記事に書いたので、ここでは「どう適用するか」だけを書く。
移植可否の前提診断
姉弟 .csproj パターンが乗るかどうかは、移植先プロジェクトの layered architecture に強く依存する。3 つを順に確認する。
1. Domain 層 (純ロジック層) が独立した asmdef で切り出されているか
Assets/Scripts/Domain/ のような専用ディレクトリと、<Project>.Domain.asmdef のような専用 assembly definition があること。asmdef がない (全部 Assembly-CSharp の闇に溶けている) なら、まず asmdef 切り出しが先。これは別タスク。
2. Domain 層が UnityEngine 参照を一切持たないか
using UnityEngine; がない、Vector3 / MonoBehaviour / ScriptableObject / Coroutine を触っていない。asmdef に "noEngineReferences": true を設定して Unity Editor 側でビルドが通るなら OK。この設定が「Engine API 触ったらコンパイル時に落ちる」safety net になる。触っているなら、Vector3 を System.Numerics.Vector3 に置き換えるか、Domain と Presentation の責務分離からやり直す。
3. EditMode テスト asmdef が Domain 層しか参照していないか
<Project>.Tests.EditMode.asmdef のような UTF asmdef の references に Domain asmdef だけが並び、UnityEngine.TestRunner 以外の Engine 系を引いていない。Unity 同梱 NUnit (3.5.x 系) で書いたテストコードはそのまま net8.0 + NUnit 3.14.0 でも動く。
3 つ全部 yes なら、以下のチェックリストで 30 分から 1 時間で乗る。どれかが no なら、まず Domain 切り出しから始める。
移植チェックリスト 7 ステップ
<repo-root>/ を Unity プロジェクトのルート (Assets/ がある階層) として、以下を順に実行する。
1. ディレクトリ作成
<repo-root>/tests-net/
を作る。Unity の Assets/ の外側 に置くのが必須。Assets/ 配下に置くと Unity Editor が .csproj や bin/ obj/ を認識して GUID 衝突や import 暴走を起こす。
2. .csproj を作成
tests-net/<ProjectName>.Tests.csproj を作る。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Assets\Scripts\Domain\**\*.cs">
<Link>Domain\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
<Compile Include="..\Assets\Tests\EditMode\**\*.cs">
<Link>Tests\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
</Project>
調整ポイント:
<Compile Include>のパスを移植先プロジェクトの実構造に合わせる (Assets/Source/Domain/など Unity 規約から外れている場合)。- 複数 Domain layer (Domain + Application) を含めたければ
<ItemGroup>を増やす。 - バージョンは AtmoTest で実証済の組み合わせ。NUnit は 3.14.0 で固定 (理由は後述)。
3. .gitignore の調整
Unity 公式の .gitignore テンプレは *.csproj と *.sln をブランケットで除外している。tests-net 用 csproj を tracked に戻す例外と、build output を ignore する追加が要る。
# Unity-generated csproj は除外したまま、姉弟 csproj だけ track
*.csproj
!tests-net/*.csproj
# tests-net の build output は ignore
tests-net/bin/
tests-net/obj/
4. Domain asmdef の noEngineReferences を確認
Assets/Scripts/Domain/<Domain>.asmdef を開いて以下が入っていることを確認。
{
"name": "<Project>.Domain",
"noEngineReferences": true,
...
}
これが入ってないと、Unity Editor 側ではビルドが通っているのに姉弟 .csproj が UnityEngine.Vector3 などを error CS0246 で吐く事故が遅れて発覚する。この設定は CI を守る最大の安全機構 なので、移植時に明示的に確認する。
5. CI workflow を追加
.github/workflows/test.yml:
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
domain:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet restore
working-directory: tests-net
- run: dotnet test --no-restore --logger "console;verbosity=normal"
working-directory: tests-net
working-directory: tests-net を 全 step で統一 する (片方だけだと dotnet test がプロジェクトルートで .csproj を見失う)。
6. ローカルで動作確認
cd <repo-root>/tests-net
dotnet restore
dotnet test
Green が出たら commit 候補ファイルは:
tests-net/<Project>.Tests.csproj(新規).gitignore(例外行追加).github/workflows/test.yml(新規 or 置換)Assets/Scripts/Domain/<Domain>.asmdef(noEngineReferences 入ってなければ追加)
7. PR を出して CI が green になるのを確認
ubuntu-latest で 1 分以内に終わるはず (AtmoTest 実測 43 秒)。落ちたら次節を確認。
つまずきポイント cheatsheet
NUnit 4 を選ぶと地雷
NUnit 4 は Assert.AreEqual / Assert.IsTrue / Assert.IsNull など Classic Assert API を NUnit.Framework.Legacy.ClassicAssert に分離 した。Unity 同梱 NUnit (3.5.x 系) で書いた既存テストコードは Classic API 前提なので、NUnit 4 を入れると CI 側で error CS0117: 'Assert' does not contain a definition for 'AreEqual' が大量発生する。3.14.0 は NUnit 3 系の最新で Unity と同じ major、テスト書き換え不要。
EnableDefaultCompileItems=false は必須
これを忘れると dotnet test が tests-net/ 直下に勝手に .cs を漁ろうとして、<Compile Include> で取り込んだ Domain ソースと重複して error CS0579 (重複属性) や error CS0260 (partial class 不一致) を吐く。
<Link> メタデータは仮想フォルダ表示専用
<Link>Domain\%(RecursiveDir)%(Filename)%(Extension)</Link> は IDE 上の仮想フォルダ表示でしかない (Visual Studio / Rider で「リンクされたファイル」マークが付く)。実体パスは Unity 側のまま。コンパイル結果には影響しないので命名は好みでよいが、Domain と Tests を仮想フォルダで分けておくと IDE のソリューションエクスプローラが見やすい。
Unity Editor は tests-net を見ない
Unity Editor が auto regenerate する .csproj は Assembly-CSharp.csproj 等の Unity 内部用 で、tests-net/ 配下には触らない。tests-net/.csproj は Unity Editor から不可視 (Assets/ 外なので)。安心して手書き管理してよい。
setup-dotnet のバージョンを固定する
dotnet-version: 8.0.x で SDK 8 系を引く。9.0.x にすると preview を引いて Restore で詰むことがあるので、安定版が出るまでは 8 で固定推奨。
適用範囲と限界
適用可
- POCO Domain 層の単体テスト
- Application 層 (UseCase / Service) も
noEngineReferencesで切れていれば同じ手順で乗る - 純粋なロジック (バリデーション、状態遷移、計算) のテスト
適用不可
- PlayMode テスト (
Coroutine、yield return new WaitForSeconds、MonoBehaviour.Updateなど Unity ランタイム必須) - MonoBehaviour ロジック (
OnTriggerEnter、Awake、OnDestroyのライフサイクル) UnityEngine.Vector3/Quaternion/Random.Rangeを直接触る箇所 (System.Numerics.Vector3で代替できなくはないが、それは Domain 設計のリファクタが必要)- ScriptableObject 経由のデータ読み込み (Editor で
.assetを import してから初めて値が入る)
これらを CI に乗せたい場合は self-hosted runner を立てるか Unity Pro ($2,200/年~) しかない (詳細は 前回記事の比較表 を参照)。
拡張パターン
複数 Domain assembly を含める
Domain + Application + Infrastructure (POCO) を全部 CI に乗せたいなら、<Compile Include> の ItemGroup を増やす:
<ItemGroup>
<Compile Include="..\Assets\Scripts\Domain\**\*.cs">
<Link>Domain\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
<Compile Include="..\Assets\Scripts\Application\**\*.cs">
<Link>Application\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
<Compile Include="..\Assets\Tests\EditMode\**\*.cs">
<Link>Tests\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
</ItemGroup>
それぞれの asmdef にも noEngineReferences: true が要る。
Code coverage を取りたい
coverlet.collector を PackageReference に追加して dotnet test --collect:"XPlat Code Coverage" で coverage report を出せる。CI で artifact upload して PR にコメントするには別途 codecov action を組む。
.NET Framework が必要な場合 (Unity 2019 系の遺産)
<TargetFramework>net8.0</TargetFramework> を net48 または netstandard2.0 に変えれば古い Unity でも動く可能性がある (未検証)。新規プロジェクトなら net8.0 + Unity 6 で揃えるのが素直。
ローカル TDD ループとの関係
Unity Editor 側の Assets/Tests/EditMode/*.asmdef は そのまま残す。日常の Red-Green-Refactor は Unity Test Framework + MCP for Unity の mcp__UnityMCP__run_tests で回す。
姉弟 .csproj は CI でのミラー実行専用。ソースの単一の真実は Unity 側 .cs ファイル、姉弟 .csproj は <Compile Include><Link> で参照しているだけなので、ローカルでテストを書き足したら自動的に CI 側でも同じテストが走る。同期は不要。
つまり開発体験は完全に Unity 側で完結し、CI だけ別ランナーが走っている、という非対称構成。これが「Unity プロジェクトの自然なワークフローを壊さずに CI を取り戻す」最小設計になる。
チェックリスト超圧縮版
新プロジェクトに乗せるとき手元で見返す用:
- Domain asmdef に
"noEngineReferences": trueあるか確認 <repo-root>/tests-net/作るtests-net/<Project>.Tests.csproj作る (テンプレ上記、<Compile Include>パスを実構造に合わせる).gitignoreに!tests-net/*.csprojとtests-net/{bin,obj}/追加.github/workflows/test.yml追加 (テンプレ上記、working-directory: tests-net全 step 統一)cd tests-net && dotnet restore && dotnet testローカル green 確認- PR → CI green 確認
なぜここで効くか
「Unity Personal は CI で死んでいる」という事実だけ知っていても、自分のプロジェクトに移植するときには毎回 0 から判断することになる。前提診断 (Domain 切り出し有無 + noEngineReferences) で「乗るか乗らないか」を最初に判定し、乗るなら 7 ステップで一気に通す、というプレイブックを 1 枚持っておくと、次の Unity プロジェクトでも 30 分以内に CI が green になる。Unity ランタイム抜きの Domain layer に閉じる前提で書ける範囲は意外と広く、layered architecture を守る規律のリターンとしても効く。
(Origin: 2026-05-09。AtmoTest で実装した姉弟 .csproj パターンを、別 Unity プロジェクトに乗せる手順を切り出して整理。前提診断と 7 ステップチェックリストの形にまとめ直すと、本番投入の判断がだいぶ楽になることに気づいた。)