miya8060
← back to blog

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 時間で乗る。

unitycidotnetgithub-actionsplaybooktesting

前回の記事 で 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 になる。触っているなら、Vector3System.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 が .csprojbin/ 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 testtests-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 テスト (Coroutineyield return new WaitForSecondsMonoBehaviour.Update など Unity ランタイム必須)
  • MonoBehaviour ロジック (OnTriggerEnterAwakeOnDestroy のライフサイクル)
  • 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.collectorPackageReference に追加して 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 を取り戻す」最小設計になる。

チェックリスト超圧縮版

新プロジェクトに乗せるとき手元で見返す用:

  1. Domain asmdef に "noEngineReferences": true あるか確認
  2. <repo-root>/tests-net/ 作る
  3. tests-net/<Project>.Tests.csproj 作る (テンプレ上記、<Compile Include> パスを実構造に合わせる)
  4. .gitignore!tests-net/*.csprojtests-net/{bin,obj}/ 追加
  5. .github/workflows/test.yml 追加 (テンプレ上記、working-directory: tests-net 全 step 統一)
  6. cd tests-net && dotnet restore && dotnet test ローカル green 確認
  7. 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 ステップチェックリストの形にまとめ直すと、本番投入の判断がだいぶ楽になることに気づいた。)