这一篇是详细介绍如何开始给 Unity 配置 TDD 环境的文章,具体工程原理见:TDD With Unity & LLM Agent-CSDN博客,这里写的是实操指南。
双轨架构

核心原则:
-
测试源码只维护一份(
Assets/Scripts/Tests/),外部工程通过<Compile Include>直接引用 -
Unity 内每个程序集对应一个外部
.csproj,依赖关系与 Unity asmdef 一致 -
外部工程捕获纯逻辑编译/测试错误,Unity 依赖的测试跳过
-
外部 .NET 工程视为真机(Player) :编译时无
UNITY_EDITOR系列宏,#if UNITY_EDITOR分支不参与编译 -
编译环境与 Unity 内部完全一致:编译器(Unity 内置 Roslyn 4.3.1)、目标框架(netstandard2.1)、语言版本(C# 9.0)、条件编译宏均与 Unity 2022.3.62 相同
编译一致性对齐
外部工程通过 Tests/Directory.Build.props 与 Unity 内部编译器对齐,参数取自 Unity 生成的 Main/Core.csproj 与 Editor/Data/DotNetSdkRoslyn(Unity 2022.3.62f3c1):
| 参数 | Unity 内部 | 外部工程 | 说明 |
|---|---|---|---|
| 编译器 | Roslyn 4.3.1(内置) | Roslyn 4.3.1(Lib/Roslyn/,离线自带) |
通过 CscToolPath/CscToolExe 指向,警告/诊断行为与 Unity 一致 |
| LangVersion | 9.0 | 9.0 | 限制 C# 语法,防止外部用 Unity 不支持的语法 |
| TargetFramework | netstandard2.1 | netstandard2.1 | 类库与 Unity API 面一致(测试工程 net9.0 承载运行) |
| DefineConstants | Player 真机宏集 | 相同 | 不含 UNITY_EDITOR 系列 ;可以包含 ODIN_INSPECTOR、STEAMWORKS_NET、DOTWEEN、UNITY_INCLUDE_TESTS 等自定宏,保证 #if 分支与真机编译一致 |
| AllowUnsafeBlocks | false | false | 与 asmdef allowUnsafeCode 一致 |
| NoWarn | 0169;USG0001 | 0169;USG0001 | 抑制与 Unity 相同的警告 |
真机视角 :外部工程模拟 Player 编译,代码中
#if UNITY_EDITOR分支(Editor 专属初始化、调试工具)不参与编译。这与 run-unity-tests.ps1(Editor 视角)互为补充------轨道 A 验证真机逻辑编译,轨道 B 验证 Editor 内完整行为。编译器版本差异实测 :改用 Unity Roslyn 4.3.1 后,
LubanUtil.cs:41的CS1998(async 方法缺 await)警告被正确暴露,而 .NET SDK Roslyn 5.6 不报告------证明编译器版本对齐是必须的。注意 :
LangVersion和DefineConstants变更后,代码中#if ODIN_INSPECTOR等分支会被激活,必须与 Unity 内验证结果一致。若 Unity 内某分支不编译,外部同样应报错------这是特性不是缺陷。
映射方法
Unity 中每个 *.asmdef 程序集,在 Tests/ 下各有一个对应的 .csproj,依赖关系与 asmdef 的引用一致。
本项目映射表
| Unity 程序集 | 外部 csproj 路径 | 产出 | 角色 |
|---|---|---|---|
| Core | Tests/Core/AsterStartGame.Core.csproj |
Core.dll |
类库,仅编译 Core 源码 |
| Code | Tests/Code/AsterStartGame.Code.csproj |
Code.dll |
类库,仅编译 Code 源码,ProjectReference 引用 Core |
| Tests | Tests/Tests/AsterStartGame.Tests.csproj |
Tests.dll |
测试项目,ProjectReference 引用 Core + Code |
外部目录结构
Tests/
├── Core/AsterStartGame.Core.csproj ← 类库,对应 Core 程序集
├── Code/AsterStartGame.Code.csproj ← 类库,对应 Code 程序集
├── Tests/AsterStartGame.Tests.csproj ← 测试工程,对应 Tests 程序集
├── Lib/UnityEngine/ ← Unity 引擎 DLL
├── Lib/Plugin/ ← 包依赖 DLL + Assets/Plugins DLL
├── Directory.Build.props ← 共享编译属性和 DLL 引用
├── run-tests.ps1
└── .gitignore
离线环境说明
Lib/ 目录包含了脱离 Unity Editor 独立编译所需的全部 DLL:
-
Lib/UnityEngine/--- 从 Unity EditorEditor/Data/Managed/UnityEngine/复制的引擎模块 DLL -
Lib/Plugin/--- 从项目Library/ScriptAssemblies/复制的包依赖 DLL +Assets/Plugins/的预编译 DLL -
Lib/Roslyn/--- 从 Unity EditorEditor/Data/DotNetSdkRoslyn/复制的编译器(Roslyn 4.3.1),保证离线环境与 Unity 内部编译器版本一致
在无 Unity Editor 的设备上只需确保 .NET SDK 9.0+ 已安装,执行 ./run-tests.ps1 即可编译测试。
更新 DLL:在 Unity 内执行
Tools/TDD Sync → 同步 Unity 真 DLL(需先创建该工具),或手动重新复制。
DLL 引用
所有外部工程通过 Tests/Directory.Build.props 共享以下 DLL 引用:
Unity 引擎模块
全部引擎模块 DLL 位于 Lib/UnityEngine/(含 UnityEngine.、UnityEditor. 等 80+ 个模块),由 Directory.Build.props 统一引用。
第三方包 DLL(Library/ScriptAssemblies)
从 Library/ScriptAssemblies/ 复制以下 DLL 到 Lib/Plugin/:
| 类别 | DLL |
|---|---|
| Unity DOTS | Unity.Entities, Unity.Entities.Hybrid, Unity.Collections, Unity.Mathematics, Unity.Transforms, Unity.Scenes, Unity.Burst, Unity.Profiling.Core |
| Unity 包 | Unity.TextMeshPro, Unity.InputSystem |
| 第三方 | UniTask, YooAsset, Luban.Runtime, Google.Protobuf, com.rlabrecque.steamworks.net |
| 其他 | Coffee.UIParticle, DOTween.Modules |
Assets/Plugins 预编译 DLL
从 Assets/Plugins/ 复制到 Lib/Plugin/:
| DLL | 来源 |
|---|---|
| Sirenix.OdinInspector.Attributes | Sirenix/Assemblies/ |
| Sirenix.Serialization | Sirenix/Assemblies/ |
| Sirenix.Utilities | Sirenix/Assemblies/ |
| DOTween | Demigiant/DOTween/ |
| DemiLib | Demigiant/DemiLib/ |
搭建步骤(已完成,供参考)
1. 配置 Unity 测试程序集
Assets/Scripts/Tests/Tests.asmdef 引用 Core 和 Code:
这部分建议在 Unity 内手动操作,通过 Window / General / Test Runner ,之后再 Unity 的交互窗口中创建测试用程序集。
{
"name": "Tests",
"references": ["Core", "Code"],
"includePlatforms": [],
"optionalUnityReferences": ["TestAssemblies"]
}
2. 创建外部 csproj
每个非测试程序集对应一个类库 csproj,测试程序集对应测试项目 csproj。三个 csproj 共享 Directory.Build.props 中的编译器属性和 DLL 引用。
3. 运行测试
cd Main/Tests
./run-tests.ps1
运行脚本与测试报告
两个脚本运行后都会生成测试报告,遵循相同的报告策略:
-
历史报告 :每次运行按时间戳命名,存放在
Tests/Log/子文件夹,该目录被 git 忽略 -
基线报告 :每次运行后将最新报告复制到
Tests/根目录,覆盖旧基线,随 git 仓库提交
run-tests.ps1(轨道 A:外部 .NET 纯逻辑测试)
$root = $PSScriptRoot
$testProj = Join-Path $root "Tests\AsterStartGame.Tests.csproj"
$logDir = Join-Path $root "Log"
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$resultFile = Join-Path $logDir "dotnet-tests-${timestamp}.trx"
$baselineFile = Join-Path $root "latest-dotnet-tests.trx"
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
dotnet test $testProj --nologo --logger "trx;LogFileName=$resultFile" 2>&1 | Out-Host
$exitCode = $LASTEXITCODE
if (Test-Path $resultFile) {
Copy-Item -Path $resultFile -Destination $baselineFile -Force
Write-Host "`nReport : $resultFile" -ForegroundColor Cyan
Write-Host "Baseline : $baselineFile" -ForegroundColor Cyan
} else {
Write-Host "[WARN] Result file not found" -ForegroundColor Yellow
}
exit $exitCode
产出:
-
历史报告:
Tests/Log/dotnet-tests-<时间戳>.trx -
基线报告:
Tests/latest-dotnet-tests.trx
run-unity-tests.ps1(轨道 B:Unity Test Framework)
产出:
-
历史报告:
Tests/Log/unity-<平台>-results-<时间戳>.xml -
历史日志:
Tests/Log/unity-<平台>-log-<时间戳>.txt -
基线报告:
Tests/latest-unity-<平台>.xml(EditMode / PlayMode)
两个脚本的基线报告文件名不同(latest-dotnet-tests.trx 与 latest-unity-*.xml),互不覆盖,均提交到 git。
新设备初始配置:Unity 编辑器路径
run-unity-tests.ps1 需要一个 Unity Editor 可执行文件路径。每台新设备首次运行前都必须配置。
配置位置(脚本顶部)
打开 Tests/run-unity-tests.ps1,找到文件顶部的 【新设备初始配置区】 ,修改 $UnityEditorPath 为本机 Unity 实际路径:
# ============================================================
# 【新设备初始配置区】请在此配置本机的 Unity 编辑器路径
# ============================================================
$UnityEditorPath = "D:\Unity\2022.3.62f3c1\Editor\Unity.exe"
常见路径示例:
| 安装方式 | 路径 |
|---|---|
| Unity Hub 默认安装 | C:\Program Files\Unity\Hub\Editor\2022.3.62f3c1\Editor\Unity.exe |
| 自定义安装 | D:\Unity\2022.3.62f3c1\Editor\Unity.exe(本项目当前用此路径) |
三种配置方式(优先级从高到低)
-
命令行参数 :
.\run-unity-tests.ps1 -TestPlatform EditMode -UnityPath "C:\...\Unity.exe" -
环境变量 :
$env:UNITY_EDITOR_PATH = "C:\...\Unity.exe"后再运行脚本 -
脚本顶部配置区 :修改
$UnityEditorPath(最常用)
未配置时的提示
若脚本找不到 Unity(配置区留空、参数/环境变量均未提供、自动探测也失败),会打印红色错误提示,引导你填写脚本顶部的配置区:
[ERROR] Unity Editor not found.
请在脚本顶部【新设备初始配置区】中配置 Unity 路径:
E:\Project\AsterStartGame\Main\Tests\run-unity-tests.ps1
或使用参数 / 环境变量指定,例如:
.\run-unity-tests.ps1 -UnityPath "D:\Unity\2022.3.62f3c1\Editor\Unity.exe"
$env:UNITY_EDITOR_PATH = "D:\Unity\2022.3.62f3c1\Editor\Unity.exe"
自动探测(无需手动配置时的兜底)
脚本依次尝试以下位置,全部命中才使用,否则报错要求配置:
-
命令行
-UnityPath参数 -
环境变量
UNITY_EDITOR_PATH -
脚本顶部
$UnityEditorPath -
Unity Hub 默认目录
C:\Program Files\Unity\Hub\Editor\<版本>\Editor\Unity.exe -
Windows 注册表
HKLM\SOFTWARE\Unity Technologies\Installer\Unity <版本>
编译一致性验证
核心目标 :确认外部 .NET 工程的编译结果与 Unity 内部真机(Player)编译一致,且两环境编译错误相同。验证通过后,其他设备默认信任外部编译,无需每台重复验证。
验证脚本:run-verify.ps1
Tests/run-verify.ps1
执行流程(4 步):
-
外部编译 :
dotnet build外部 Core 工程,统计error CS数 -
Unity 真机编译 :命令行调 Unity 执行
TddVerify.TddVerifyBuild.CompilePlayerAssemblies(BuildPipeline.BuildPlayer+BuildScriptsOnly),统计 Unity 日志中error CS数 -
产物对比 :对比 Unity 真机产物(
Library/Bee/PlayerScriptAssemblies/Core.dll)与外部产物-
业务引用集(排除 Unity 生成代码引入的引用)
-
业务类型表面(排除 Unity 自动生成的辅助类型)
-
-
结论 :PASS / FAIL,报告写入
Tests/Log/verify-<时间戳>.txt
验证判据
| 判据 | 通过条件 |
|---|---|
| 编译错误一致 | 外部 dotnet build 错误数 = Unity 真机编译错误数(正常源码均为 0) |
| 业务引用集一致 | 两 Core.dll 排除生成代码引入引用后完全一致 |
| 业务类型表面一致 | 两 Core.dll 排除生成辅助类型后完全一致 |
允许的预期差异(非缺陷)
Unity 内部编译会自动生成两类辅助代码,外部工程不包含(不影响编译错误与业务逻辑):
| 生成类型 | 来源 | 说明 |
|---|---|---|
AssemblyTypeRegistry |
Unity.Entities 源码生成器 | Entities 运行时类型注册表 |
UnitySourceGeneratedAssemblyMonoScriptTypes_v1 |
Unity 内置 MonoScript 生成器 | 编辑器 MonoScript 序列化映射 |
因此 Unity 真机产物会比外部多出上述类型及其引入的程序集引用(Unity.Entities、Unity.Burst),脚本已自动排除,不会误报 FAIL。
固化标准
-
验证通过后 :其他设备默认信任外部编译结果,直接使用
run-tests.ps1,无需每台跑run-verify.ps1 -
需要重新验证 :Unity 升级、Entities/包版本变化、
Directory.Build.props编译参数变更 -
排查 :若某次出现"外部编译通过但 Unity 真机报错"或反之,运行
run-verify.ps1定位差异
注意:Unity 真机编译依赖
Library/Bee/PlayerScriptAssemblies/缓存。首次运行或清理缓存后,脚本会自动触发 Unity 编译。若 BuildPlayer 因 DOTS 场景烘焙失败(退出码 2),脚本仍会复制已编译产物并继续验证------因为场景烘焙失败不代表脚本编译失败。
.gitignore
**/bin/
**/obj/
/Log/
*.user
*.suo
常见问题
外部编译报 CS0246/CS0436
| 报错 | 修法 |
|---|---|
CS0246: type 'X' not found |
缺 DLL 引用 → 从 Unity Editor 或 Library/ScriptAssemblies 同步 |
CS0436: 'X' conflicts with imported type |
DLL 重复 → 移除冗余 Reference |
CS0012: type 'X' in un-referenced assembly |
类型定义在未引用的 DLL(如 Sirenix.Serialization 的 SerializedScriptableObject)→ 在 Directory.Build.props 添加该 DLL 引用 |
外部编译通过但 Unity 内运行报错
外部工程只做静态编译检查,如果涉及 UnityEngine 的代码,大概率是无法正常运行的。因此,运行时行为(MonoBehaviour 生命周期、GameObject 实例化等)必须在 Unity Test Runner(轨道 B)中验证。
缺少 DLL 引用
在 Unity 内使用编辑器辅助工具 Tools/TDD Sync → 同步 Unity 真 DLL 一键同步。手动方式:从 Unity Editor 安装目录 Editor/Data/Managed/UnityEngine/ 复制引擎 DLL,从项目 Library/ScriptAssemblies/ 复制包依赖 DLL。
关于这部分,我写了一个示例框架工程,大家可以参考使用;