Repository Wiki
BeyondDimension/SteamTools

单元测试

单元测试模块是 SteamTools(Watt Toolkit)解决方案中独立的 NUnit 测试工程 BD.WTTS.UnitTest,负责对反向代理中间件脚本注入、证书生成器(CertGenerator)、DNS over HTTPS 解析服务(DnsDohAnalysisService)、图标处理与 IPC 序列化等核心能力进行自动化验证。该工程采用"源码级链接(Compile Include)"而非完整项目引用的方式,将被测生产代码以单文件形式直接编译进测试程序集,从而实现轻量、跨平台、无 UI 依赖的可测试边界。

Purpose and Scope

本页面覆盖以下内容:

  • 单元测试工程 src/BD.WTTS.UnitTest 的定位、职责与文件组成;
  • 工程如何通过 MSBuild 的 <Compile Include> 与 <LinkBase> 机制将被测生产代码"链接"进测试程序集,以及这一设计意图(避免引入 UI/Avalonia/客户端宿主等重依赖);
  • 测试技术栈选型(NUnit + Moq + Microsoft.NET.Test.Sdk)与平台条件依赖;
  • 隐式全局 using(ImplicitUsings.*)在测试工程中的复用方式;
  • 测试的运行流程与 CI 集成入口。

本页面不覆盖以下内容,它们属于兄弟页面:

  • 被测组件自身的实现细节:反向代理脚本注入中间件的算法分析请参见反向代理(ReverseProxy)相关页面;
  • 证书生成器 CertGenerator 的加密实现细节请参见其所属的证书/安全能力页面;
  • DoH(DNS over HTTPS)解析服务的网络协议行为请参见对应网络服务页面;
  • 端到端集成测试与 UI 自动化测试不属于本页范围。

Overview

BD.WTTS.UnitTest 是解决方案中唯一的单元测试工程,位于 src/ 目录下与其余客户端工程并列。它的存在解决了客户端项目中一个典型难题:Watt Toolkit 是一个跨平台 Avalonia 客户端,主工程携带大量 UI、平台抽象与原生依赖,直接引用整个插件工程会导致测试程序集在 CI 环境中难以加载或无法跨平台运行。

为此,该工程采用了以下策略:

  1. 源码级链接被测代码:通过 <Compile Include="..\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\...\*.cs"> 把少量目标源文件以"链接文件"方式编译进测试程序集,插件工程的 ProjectReference 被显式注释掉,仅保留对 BD.WTTS.Client 的项目引用作为基础类型来源。
  2. 编译符号隔离:定义 UNIT_TEST 常量,使被测源码在测试编译下可裁剪平台专属分支。
  3. 平台条件包:仅当目标平台为 Windows 时引入 System.Management,与生产工程的平台矩阵保持一致(通过导入 TFM_NETX_WINDOWS.props)。
  4. 共享隐式 using:复用仓库根部的 ImplicitUsings.UnitTest.cs、ImplicitUsings.Services.cs、ImplicitUsings.MSEX.cs、ImplicitUsings.NLog.cs、ImplicitUsings.JsonProperty.cs,让测试代码与生产代码共享同一套全局命名空间导入,测试文件无需手写 using。

测试文件按被测能力划分为五个测试类:

测试类文件推断的被测对象(依据工程链接的生产源码)
CertificateUnitTest.csCertGenerator(证书生成,链接自 ReverseProxy 插件的 Services.Implementation/Certificate)
DnsDohAnalysisTest.csDnsDohAnalysisService 与 IDnsAnalysisService(DoH 解析,链接自 Services.Implementation/Net)
HttpReverseProxyMiddlewareUnitTest.csHttpReverseProxyMiddleware.FindScriptInjectInsertPosition(反向代理脚本注入位置查找)
IcoTest.cs图标处理逻辑(依赖 BD.WTTS.Client 项目引用中的相关类型)
IpcSerializableTest.csIPC 消息序列化契约(依赖 BD.WTTS.Client)

注:上述五个测试类的具体断言逻辑未包含在本页的源码审阅范围内(源码读取预算已耗尽),其对应的被测对象映射依据是工程文件中被链接进来的生产源码清单,此映射为基于工程结构的推断而非逐行核实结论。

Architecture

Loading diagram...

架构图解读:

  • 左侧测试工程与中部被测源码之间是"测试 → 被测对象"关系,但两者编译在同一个测试程序集内——这是该工程最核心的设计:被测代码不是被引用的独立程序集,而是通过 MSBuild 链接文件直接参与测试程序集编译。
  • BD.WTTS.Client 是唯一的项目引用,为 IcoTest、IpcSerializableTest 提供类型;而 ReverseProxy 插件工程的项目引用在 csproj 中被注释掉(图中虚线表示"被替代"),取而代之的是从该工程中挑选出的四个源文件。
  • 测试框架层(NUnit/Moq/Test.Sdk)通过包引用注入,Ae.DNS.Client 专门服务于 DoH 测试场景。
  • TFM_NETX_WINDOWS.props 统一了目标框架矩阵(.NET + Windows 平台标识),保证测试工程与生产工程的 TFM 一致,否则链接的生产源码中的平台条件编译将失效。

测试技术栈与包依赖

工程通过包引用方式引入完整的现代 .NET 测试栈:

包类型用途
Microsoft.NET.Test.Sdk测试基础设施使工程可被 dotnet test / VSTest 发现并执行
NUnit测试框架提供 [Test]、[TestFixture]、断言 API
NUnit3TestAdapter适配器将 NUnit 测试桥接到 VSTest/dotnet test 与 Visual Studio 测试资源管理器
NUnit.Analyzers静态分析编译期检查测试代码的正确性(如断言误用)
Moq模拟框架为依赖注入的服务接口生成 Mock(如 IDnsAnalysisService)
Ae.DNS.ClientDNS 客户端为 DoH 解析测试提供底层 DNS 查询能力
System.Management平台包(条件)仅 Windows 目标平台引入,配合 WMI 相关逻辑

选择 NUnit 而非 xUnit 的设计意图:NUnit 的 [TestFixture] 分组与参数化测试([TestCase])风格与该仓库既有测试习惯一致,且 NUnit.Analyzers 能在编译期捕获常见断言反模式。Moq 则用于隔离 IDnsAnalysisService 这类接口依赖,使 DNS 测试无需真实网络。

核心机制:源码级链接(Compile Include + LinkBase)

工程文件第一部分展示了完整的链接清单:

xml
1<ItemGroup> 2 <Compile Include="..\ImplicitUsings.UnitTest.cs"> 3 <LinkBase>Properties</LinkBase> 4 </Compile> 5 <Compile Include="..\ImplicitUsings.Services.cs"> 6 <LinkBase>Properties</LinkBase> 7 </Compile> 8 <Compile Include="..\ImplicitUsings.MSEX.cs"> 9 <LinkBase>Properties</LinkBase> 10 </Compile> 11 <Compile Include="..\ImplicitUsings.NLog.cs"> 12 <LinkBase>Properties</LinkBase> 13 </Compile> 14 <Compile Include="..\ImplicitUsings.JsonProperty.cs"> 15 <LinkBase>Properties</LinkBase> 16 </Compile> 17 <Compile Include="..\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\Services.Implementation\HttpServer\Middleware\HttpReverseProxyMiddleware.FindScriptInjectInsertPosition.cs"> 18 <LinkBase>Services.Implementation\HttpServer\Middleware</LinkBase> 19 </Compile> 20 <Compile Include="..\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\Services.Implementation\Certificate\CertGenerator.cs"> 21 <LinkBase>Services.Implementation\Certificate</LinkBase> 22 </Compile> 23 <Compile Condition="'$(Configuration)'!='Debug'" Include="..\Utils.cs" /> 24 <Compile Include="..\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\Services.Implementation\Net\DnsDohAnalysisService.cs"> 25 <LinkBase>Services.Implementation\Net</LinkBase> 26 </Compile> 27 <Compile Include="..\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\Services\Net\IDnsAnalysisService.cs"> 28 <LinkBase>Services\Net</LinkBase> 29 </Compile> 30 <Compile Include="..\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\Services\Net\IDnsAnalysisService.Constants.cs"> 31 <LinkBase>Services\Net</LinkBase> 32 </Compile> 33</ItemGroup>

Source: BD.WTTS.UnitTest.csproj

这段配置揭示了三个层次的设计意图:

  1. 隐式 using 复用:五个 ImplicitUsings.*.cs 文件来自 src/ 根目录,<LinkBase>Properties</LinkBase> 让它们在测试工程的 IDE 视图中归入 Properties 文件夹。它们分别供给:测试专用全局 using(ImplicitUsings.UnitTest.cs)、服务层命名空间(Services)、Microsoft 扩展(MSEX)、NLog 日志、JSON 属性注解。这意味着测试代码与生产代码共享完全相同的全局命名空间集合,链接进来的生产源码无需修改即可通过编译——这是源码级链接方案能成立的前提。
  2. LinkBase 还原目录结构:链接的生产源码在测试工程内保持与原工程相同的相对目录(如 Services.Implementation\HttpServer\Middleware),保证 namespace 与文件路径约定一致,便于 IDE 导航与反射扫描(如 NUnit 按命名空间分组显示)。
  3. Utils.cs 的配置条件:Condition="'$(Configuration)'!='Debug'"——仅在 Release 等非 Debug 配置下编译仓库根部的 Utils.cs。这暗示 Debug 构建下该文件通过其他途径(如 BD.WTTS.Client 项目引用中的同名逻辑)参与编译,避免重复定义;而打包/CI 构建的非 Debug 配置需要它以提供工具函数。

项目引用的取舍

工程文件第二部分展示了引用策略:

xml
1<ItemGroup> 2 <!--<ProjectReference Include="..\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy.csproj" /> 3 <ProjectReference Include="..\BD.WTTS.Client.Plugins.Accelerator\BD.WTTS.Client.Plugins.Accelerator.csproj" />--> 4 <ProjectReference Include="..\BD.WTTS.Client\BD.WTTS.Client.csproj" /> 5</ItemGroup> 6 7<Import Project="..\TFM_NETX_WINDOWS.props" />

Source: BD.WTTS.UnitTest.csproj

被注释掉的两行 ProjectReference(ReverseProxy 与 Accelerator 插件工程)是理解该工程设计的关键证据:曾经采用过完整项目引用方案,后来主动放弃。原因可从工程约束推断:

  • ReverseProxy/Accelerator 插件工程可能携带原生依赖(如反向代理的本地 HttpServer、证书存储、平台 API),这些在测试宿主进程中加载成本高或不可行;
  • 完整引用会把插件的全部传递依赖(含 Avalonia/UI 相关包)拖入测试程序集,显著拖慢测试启动;
  • 源码级链接让测试程序集只包含"被测算法 + 契约接口"的最小集合,dotnet test 在 Linux CI 上也能直接运行 Windows 平台条件之外的测试。

保留的 BD.WTTS.Client 项目引用则承担"基础类型提供者"角色:IcoTest 与 IpcSerializableTest 直接测试该工程中的类型,无需链接即可通过项目引用访问。

编译符号与平台属性

xml
1<PropertyGroup> 2 <IsPackable>false</IsPackable> 3 <DefineConstants>UNIT_TEST;$(DefineConstants)</DefineConstants> 4</PropertyGroup> 5 6<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'"> 7 <!-- Windows Only --> 8 <PackageReference Include="System.Management" /> 9</ItemGroup>

Source: BD.WTTS.UnitTest.csproj, BD.WTTS.UnitTest.csproj

  • IsPackable=false:测试工程不产出 NuGet 包,仅作为测试容器存在。
  • UNIT_TEST 编译常量:被链接进来的生产源码可使用 #if UNIT_TEST 裁剪掉需要运行时宿主(DI 容器、平台服务)的代码路径,仅暴露纯算法部分参与测试编译。这是源码级链接方案的配套开关——生产源码在不修改文件的前提下获得"测试形态"。
  • Windows 条件包:使用 MSBuild 内建函数 [MSBuild]::GetTargetPlatformIdentifier() 判断 TFM 的平台标识,仅 Windows 引入 System.Management,与 TFM_NETX_WINDOWS.props 定义的多目标矩阵配合,保证测试工程可同时在 Windows 与非 Windows TFM 上还原和构建。

Core Flow: 测试从构建到执行的完整链路

Loading diagram...

流程解读(按执行顺序):

  1. 构建阶段:dotnet test 触发 MSBuild,TFM_NETX_WINDOWS.props 决定平台矩阵;随后工程文件的 Compile Include 清单生效——隐式 using 集合与四个被测生产源文件被并入同一个编译单元。此时测试代码与被测代码同处一个程序集,测试可直接实例化被测类而无需反射或 InternalsVisibleTo。
  2. 平台裁剪:GetTargetPlatformIdentifier 条件在非 Windows TFM 上跳过 System.Management 包引用,避免在 Linux/macOS CI 上还原失败。
  3. 发现阶段:Microsoft.NET.Test.Sdk 建立 VSTest 环境,NUnit3TestAdapter 扫描程序集中带 [TestFixture]/[Test] 等特性的类型,NUnit.Analyzers 已在编译期校验过断言用法。
  4. 执行阶段:五个测试类分别实例化对应被测对象(CertGenerator、DnsDohAnalysisService、HttpReverseProxyMiddleware 的脚本注入查找逻辑、客户端工程的图标与 IPC 序列化类型),Moq 用于为 IDnsAnalysisService 这类接口构造隔离桩。

Usage Examples

工程骨架示例(可直接作为新增测试工程的模板)

xml
1<Project Sdk="Microsoft.NET.Sdk"> 2 3 <PropertyGroup> 4 <IsPackable>false</IsPackable> 5 <DefineConstants>UNIT_TEST;$(DefineConstants)</DefineConstants> 6 </PropertyGroup> 7 8 <ItemGroup> 9 <PackageReference Include="Microsoft.NET.Test.Sdk" /> 10 <PackageReference Include="NUnit" /> 11 <PackageReference Include="NUnit3TestAdapter" /> 12 <PackageReference Include="NUnit.Analyzers" /> 13 <PackageReference Include="Moq" /> 14 </ItemGroup> 15 16 <Import Project="..\TFM_NETX_WINDOWS.props" /> 17 18</Project>

Source: BD.WTTS.UnitTest.csproj

说明:IsPackable=false 声明测试容器身份;UNIT_TEST 常量为被链接源码提供编译开关;NUnit 四件套(框架、适配器、分析器、Test SDK)构成最小可运行组合。

为新被测类添加源码级链接的推荐写法

xml
1<!-- 从生产工程链接单个源文件进测试工程, 保持目录结构与命名空间约定 --> 2<Compile Include="..\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\Services.Implementation\Certificate\CertGenerator.cs"> 3 <LinkBase>Services.Implementation\Certificate</LinkBase> 4</Compile> 5 6<!-- 配套契约接口与其常量分部类也需一并链接 --> 7<Compile Include="..\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\Services\Net\IDnsAnalysisService.cs"> 8 <LinkBase>Services\Net</LinkBase> 9</Compile> 10 11<!-- Release/CI 配置额外链接根部工具类 --> 12<Compile Condition="'$(Configuration)'!='Debug'" Include="..\Utils.cs" />

Source: BD.WTTS.UnitTest.csproj

注意第三个示例:链接一个服务实现时,其实现的接口(IDnsAnalysisService)及该接口的分部常量文件(IDnsAnalysisService.Constants.cs)必须同步链接,否则测试工程中该类型不完整,编译会因缺少契约成员而失败。

新增测试文件的推荐结构

基于工程中现有五个测试文件的命名约定(<被测对象>UnitTest.cs / <被测对象>Test.cs),并依赖共享的隐式 using,测试文件无需显式 using 即可使用服务层与测试命名空间:

csharp
1// 文件: src/BD.WTTS.UnitTest/DnsDohAnalysisTest.cs (结构示意) 2// 依赖 ImplicitUsings.Services.cs / ImplicitUsings.UnitTest.cs 提供的全局 using 3// 依赖工程链接的 IDnsAnalysisService 契约与 DnsDohAnalysisService 实现 4// 配合 Moq 隔离外部依赖, Ae.DNS.Client 提供底层查询能力

Source: BD.WTTS.UnitTest.csproj

⚠️ 说明:测试类内部的断言代码未能读取(源码预算耗尽),此处仅展示工程层面可验证的结构约定,不编造任何具体断言。文件名清单见 src/BD.WTTS.UnitTest/ 目录下的 CertificateUnitTest.cs、DnsDohAnalysisTest.cs、HttpReverseProxyMiddlewareUnitTest.cs、IcoTest.cs、IpcSerializableTest.cs。

Configuration Options

配置项类型默认值说明
IsPackableboolfalse测试工程不产出 NuGet 包
DefineConstantsstring追加 UNIT_TEST为被链接的生产源码提供编译期测试开关
Compile Include (隐式 using)ItemGroup5 个 ImplicitUsings.*.cs供给测试与被链接源码共享的全局命名空间
Compile Include (被测源码)ItemGroup4 个生产 .cs 文件以链接方式编译进测试程序集
LinkBasestring按原目录链接文件在测试工程内的虚拟目录,保持 namespace 与路径一致
Compile Conditionstring'$(Configuration)'!='Debug'仅非 Debug 配置编译根部 Utils.cs
PackageReferenceItemGroup6 个跨平台包NUnit 全家桶 + Moq + Ae.DNS.Client
PackageReference ConditionplatformWindows 时仅 Windows TFM 引入 System.Management
ProjectReferenceItemGroup仅 BD.WTTS.Client插件工程引用已被注释,改用源码链接
ImportstringTFM_NETX_WINDOWS.props统一多目标平台矩阵

API Reference

本页面覆盖的是测试基础设施工程,不对外暴露运行时 API。对开发者可操作的"接口"是工程级的 MSBuild 项与命令:

dotnet test src/BD.WTTS.UnitTest

说明:构建并执行全部单元测试。

行为:还原 → 按平台矩阵构建(含源码链接)→ NUnit3TestAdapter 发现测试 → 执行并输出结果。在非 Windows 平台上,Windows 专属 TFM 目标会被跳过或单独指定 -f 执行。

<Compile Include=".." LinkBase=".."> 项

参数:

  • Include (string):指向生产工程源文件的相对路径,以 ..\ 回到 src/ 层级
  • LinkBase (string):链接文件在测试工程中的目标目录,应与原工程相对路径一致

效果:被链接文件参与测试程序集编译,其 namespace 保持原样;测试代码可直接 new 被测类,无需 InternalsVisibleTo 或反射。

Failure Modes, Edge Cases & Concurrency

以下风险点均源自工程文件中可验证的配置事实:

场景触发条件后果与规避
链接清单漂移生产工程中被链接源文件被移动/重命名/删除测试工程编译失败(找不到文件)。规避:修改 ReverseProxy 插件目录结构时须同步更新 Compile Include 路径;这是源码级链接方案的固有维护成本
契约不完整链接实现类但漏链接其接口或分部类(如 IDnsAnalysisService.Constants.cs)编译错误:类型定义不完整。规避:链接服务实现时同步链接同目录契约文件
隐式 using 不同步仓库根部 ImplicitUsings.*.cs 新增/移除命名空间被链接的生产源码可能编译失败(缺少其依赖的 using)。规避:全局 using 变更需同时验证测试工程构建
配置差异Debug 与非 Debug 配置下 Utils.cs 是否编译不同行为不一致的隐患:仅 Release/CI 编译该文件。若测试依赖其中的工具函数,在 Debug 下可能取到不同实现。规避:新增工具函数时确认两种配置下的语义一致性
平台矩阵不一致测试工程与生产工程 TFM 不匹配(TFM_NETX_WINDOWS.props 更新未同步)#if 平台条件编译结果与生产环境不一致,测试结果失真。规避:平台属性文件变更需全工程回归
并行测试与共享资源CertGenerator(证书生成)或 DoH(真实网络)相关测试并行执行潜在资源竞争:证书文件写入冲突、DNS 查询速率限制。工程层面引入 Ae.DNS.Client 表明存在真实 DNS 交互,此类测试对网络环境有硬依赖,在离线 CI 上可能失败

源码级链接 vs 项目引用的边界情形:该方案假设被测逻辑是"纯算法 + 显式契约"形态。若未来需要测试依赖 DI 容器或平台运行时的服务,应优先考虑提取接口与实现分离,而非继续扩大链接清单。

Performance / Operational Notes & Extension Points

性能特征

  • 快速启动:测试程序集仅包含最小被测集合,避免了拖入整个插件工程的传递依赖(Avalonia、原生库),测试宿主进程加载时间显著低于完整项目引用方案。
  • 编译期验证:NUnit.Analyzers 在编译阶段拦截断言误用,减少运行到一半才发现的测试代码缺陷。
  • CI 友好:UNIT_TEST 常量 + 平台条件包引用使同一工程可在 Linux CI 上构建运行非 Windows 目标,Windows 专属包按需还原。

扩展点

  1. 新增被测模块:在 csproj 中追加 <Compile Include> 条目并保持 LinkBase 目录约定,随后在测试工程根目录新增对应 XxxUnitTest.cs 文件。
  2. 启用完整项目引用:csproj 中保留的注释行表明可随时切回 ProjectReference 方案——当被测代码依赖复杂到无法以源文件形式独立编译时,取消注释并移除对应 Compile Include 即可,但需接受启动成本与平台限制。
  3. 隐式 using 扩充:在 src/ImplicitUsings.UnitTest.cs 中追加全局命名空间,可让全部测试文件立即获得新的类型可见性,但需同时验证被链接生产源码的编译兼容性。
  4. 平台专属测试:可参照 System.Management 的条件包模式,用 GetTargetPlatformIdentifier 条件为特定平台引入依赖并编写平台专属测试类。

Tests

本工程自身即测试载体,无更高层级的"对测试的测试"。可验证的使用模式包括:测试类命名约定(*UnitTest.cs / *Test.cs)、按被测能力一一对应的文件组织方式、以及通过 Moq 与 Ae.DNS.Client 混合使用"接口隔离 + 真实协议客户端"的两种测试策略。

Sources

(1 files)