xunit.v3 迁移实战指南:将 .NET 测试项目从 xUnit.net v2 平滑升级到 xUnit.net v3
【免费下载链接】skillsRepository for skills to assist AI coding agents with .NET and C#项目地址: https://gitcode.com/GitHub_Trending/skills17/skills
xUnit.net v3 是 xUnit.net 测试框架的下一代版本,重构了包结构、运行器与扩展点,从xunitv2 包直接升级到xunit.v3会遭遇编译错误、破坏性 API 变更与运行器配置问题。本文基于 dotnet-skills 仓库中的migrate-xunit-to-xunit-v3技能文档(SKILL.md),给出从 v2 到 v3 的完整迁移工作流:包引用映射、OutputType=Exe、VSTest/MTP 运行器保留、CPM 中心包管理适配,以及async void、类型化属性、自定义 Fact/Theory/BeforeAfterTest 属性、SkippableFact、Combinatorial/StaFact 配套包等破坏性变更的系统化处理。读完本文,你将能独立完成一次可构建、可发现、可运行的 xUnit.net v3 迁移,并理解每一步背后的设计动机与验证标准。
迁移目标与适用边界
迁移的最终形态
xUnit.net v3 迁移的完成标准非常明确:解决方案中所有测试项目引用xunit.v3.*包族、项目能够干净编译、且全部测试以与迁移前相同的结果通过。注意这里特别强调"相同的结果"——迁移不应改变测试语义,也不应悄悄吞掉或新增跳过。
何时使用本技能
- 将测试项目从
xunit(v2)系列包升级到xunit.v3; - 更新 xunit 包引用到 v3 后,需要解决随之而来的编译错误。
何时不要使用本技能
- 框架间迁移:在 MSTest、NUnit 与 xUnit.net 之间切换属于完全不同的工作,应使用仓库中对应的
migrate-*-to-mstest系列技能(如 migrate-nunit-to-mstest),而不是本技能; - 仅运行器迁移:从 VSTest 迁移到 Microsoft.Testing.Platform(MTP)应使用 migrate-vstest-to-mtp,其中也包含 xUnit v3 的 MTP 过滤器语法说明;
- 项目已经引用
xunit.v3:迁移已完成,直接报告无需操作即可。
输入
| 输入 | 是否必需 | 说明 |
|---|---|---|
| 测试项目或解决方案 | 否 | 在当前工作目录中发现.csproj、.sln、.slnx、中心属性文件与源码;只有未发现任何目标或目标有歧义时才询问 |
工作区与完成契约
技能激活不等于完成。对于迁移/修复/更新类请求,必须在同一任务中检查暂存文件、编辑文件并运行测试。要点如下:
- 技能基础目录只包含指引,应搜索当前工作目录并按返回的路径打开文件;若某个工具拒绝刚搜索到的路径,改用其他可用的读取器/编辑器重试,而不是断定文件缺失;
- 不要在工作区发现能定位路径的情况下让用户提供路径;
- 一次性盘点项目/中心包文件与所有受影响源码——仅做包级迁移而在源码中遗留 v2-only API 是不完整的;
- 结束时报告:检测到的源码版本与运行器、精确的包兼容集、变更的文件、发现/通过/失败/跳过的测试计数,以及平台相关结果。没有测试发现的构建不算成功;
- 包的可用性是经验事实:记录配置的源查询或解析出的包图以及一次成功的 restore。最终结果必须说明所选精确版本是如何被证明可用的,而不是只报一个版本号让评审者去猜。
迁移前的决策矩阵
开始编辑前,先运行一次预检,根据检测到的状态决定行动:
| 检测到的状态 | 必需动作 |
|---|---|
项目引用xunit.v3,具备所需的可执行文件/运行器配置,无残留 v2 包或 v2-only API 模式,现有测试命令通过 | 停止:迁移已完成。不更新版本、不创建 props 文件、不修改源码,报告已核验的无操作结果。若仍有必要的 v3 适配,只做那一处修复,而不是把包引用本身当作完成 |
xUnit v2 使用YTest.MTP.XUnit2 | 保留 MTP:移除该 shim,设置UseMicrosoftTestingPlatformRunner=true,不要添加xunit.runner.visualstudio,也不要设置IsTestingPlatformApplication=false |
| xUnit v2 未使用 MTP shim | 保留 VSTest:保留/更新xunit.runner.visualstudio并设置IsTestingPlatformApplication=false |
自定义类型派生自BeforeAfterTestAttribute | 保留该继承关系及其行为。为两个 override 添加IXunitTest参数并传给base.Before/base.After;不要用直接实现接口的方式替换子类 |
| 基于 Type 的 collection/orderer 属性指向自定义类型 | 同时迁移属性语法与被引用类型的 v3 契约。collection factory 必须实现 xUnit v3 的IXunitTestCollectionFactory行为;只让属性编译通过而留下空工厂不算完整迁移 |
| 存在配套包 | 将xunit.v3、Xunit.Combinatorial、Xunit.StaFact 作为一组兼容集从配置源解析。若最新的 xunit.v3 主版本在源上没有兼容的稳定配套包,选择最新的兼容 xunit.v3 主版本并说明固定原因。验证测试发现而不只是编译 |
OutputType=Exe使net*-windows项目在非 Windows 主机上失败 | 在需要跨平台构建时添加EnableWindowsTargeting=true后重跑。不要把这种迁移引发的失败当作既有问题忽略 |
版本必须从配置的包源解析,不要凭产品名里的"v3"猜测版本,也不要去更新无关包。只修改包含适用规则所要求的包、属性或源码构造的文件。
Central Package Management 项目的读回检查
编辑完 CPM 项目后,同时读回Directory.Packages.props与项目文件,确认:
PackageVersion拥有版本号;- 重命名后的
PackageReference无版本号; OutputType=Exe生效。
仓库的 CPM 迁移夹具正是这一场景的实证:migrate-xunit-v2-packages-managed-via-central-package-manage 下的Directory.Packages.props以ManagePackageVersionsCentrally=true管理xunit2.9.3 与xunit.runner.visualstudio2.8.2,对应的求值测试(见 eval.yaml 的对应场景)会校验PackageVersion条目被更新为xunit.v3、csproj 中的PackageReference被重命名且去版本化,并最终以dotnet test -p:TreatWarningsAsErrors=true是否通过来裁决。
Step 1:识别 xUnit.net 项目并验证兼容性
搜索引用 xUnit.net v2 包的测试项目,v2 包名清单如下:
xunitxunit.abstractionsxunit.assertxunit.corexunit.extensibility.corexunit.extensibility.executionxunit.runner.visualstudio
包引用可能出现在项目文件中,也可能出现在 MSBuild props/targets 文件(Directory.Build.props、Directory.Build.targets、Directory.Packages.props)中,必须全部检查。
目标框架兼容性是第一个硬性关卡:xUnit.net v3 要求.NET 8+或.NET Framework 4.7.2+;测试库项目还支持 .NET Standard 2.0。若任一测试项目的目标框架不兼容,立即停止,告知用户先升级目标框架。同时确认项目使用 SDK 风格格式。
仓库夹具 detect-incompatible-target-framework-and-stop-migration 中的LegacyTests.csproj目标是net462——低于 v3 要求的最低 .NET Framework 4.7.2。对应求值场景(eval.yaml)要求:识别出 net462 低于最低要求、停止迁移、不把包引用更新为xunit.v3,并建议先升级目标框架。
Step 2:更新包引用
按以下映射更新所有PackageReference/PackageVersion项:
| v2 包名 | v3 处理方式 |
|---|---|
xunit | →xunit.v3 |
xunit.abstractions | 彻底移除 |
xunit.assert | →xunit.v3.assert |
xunit.core | →xunit.v3.core |
xunit.extensibility.core与xunit.extensibility.execution | →xunit.v3.extensibility.core(同一项目中同时引用两个时合并为单一条目,因为 v3 中两个包已合并) |
随后查询配置的包源,固定实际存在的最新稳定版本。xunit.runner.visualstudio只在 VSTest 项目中更新;MTP 项目不要添加它。
仓库夹具 migrate-basic-xunit-net-v2-project-to-v3 给出了典型 v2 项目形态:TestProject.csproj引用xunit2.9.3 与xunit.runner.visualstudio2.8.2,同时搭配Microsoft.NET.Test.Sdk18.3.0。而 consolidate-xunit-extensibility-packages-and-remove-xunit-ab 夹具则覆盖了同时引用xunit.extensibility.core、xunit.extensibility.execution与xunit.abstractions的合并场景,其求值断言xunit.v3.extensibility出现、using Xunit.Abstractions;消失。
Step 3:设置OutputType为Exe
在每个测试项目(测试库项目除外)的项目文件中设置OutputType:
<PropertyGroup> <OutputType>Exe</OutputType> </PropertyGroup>xUnit.net v3 的测试程序集是自承载的可执行文件,这一步不可或缺。根据解决方案结构,可能有集中放置的位置:
- 若所有测试项目共享(或可以共享)一个公共
Directory.Build.props,把该属性加在那里。注意:OutputType不应添加到Directory.Build.targets; - 若所有测试项目共享命名模式(如
*.Tests.csproj),可在Directory.Build.props中添加仅作用于这些项目的条件属性组,例如<OutputType Condition="$(MSBuildProjectName.EndsWith('.Tests'))">Exe</OutputType>,按需调整条件以精确命中测试项目; - 否则,在每个测试项目文件中单独添加该属性。
Step 4:配置测试平台
保留 v2 时期使用的同一测试平台是迁移的基本原则:xUnit.net v2 除使用YTest.MTP.XUnit2的项目外,一律使用 VSTest。
情况 A:项目曾引用YTest.MTP.XUnit2(MTP 场景)
- 完全移除对
YTest.MTP.XUnit2的引用; - 在已有的共享
Directory.Build.props(无共享 props 文件时放在测试项目中)设置<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>; - 不要添加
xunit.runner.visualstudio——它是 VSTest 运行器,会削弱平台保留。
仓库夹具 migrate-project-with-ytest-mtp-xunit2-to-xunit-net-v3-preser 完整呈现了这一场景:v2 项目同时引用xunit2.9.3 与YTest.MTP.XUnit21.0.0,共享Directory.Build.props目前只有IsPackable=false。其求值用dotnet msbuild TestProject.csproj -getProperty:UseMicrosoftTestingPlatformRunner验证属性最终为true,并断言TestProject.csproj中不出现xunit.runner.visualstudio、输出中不出现IsTestingPlatformApplication=false。
情况 B:项目未引用YTest.MTP.XUnit2(常见情况,VSTest 场景)
- 在已有的共享
Directory.Build.props(无共享 props 文件时直接放在测试项目中)设置<IsTestingPlatformApplication>false</IsTestingPlatformApplication>; - 不要仅为单个项目创建仓库级 props 文件——这会把项目留在 VSTest 上。
Step 5:移除Xunit.Abstractionsusing
在 C# 文件中查找using Xunit.Abstractions;指令并彻底删除。v3 中xunit.abstractions包被移除,其 API 已并入核心,保留该 using 将导致编译失败。
Step 6:处理async void破坏性变更(按需)
xUnit.net v3不再支持async void测试方法,此类代码将无法编译。搜索所有以async void声明的测试方法(可通过[Fact]、[Theory]或其他测试属性识别),改为async Task。
夹具 convert-async-void-test-methods-to-async-task 的AsyncTests.cs中,[Fact] GetUser_ReturnsExpectedName与[Theory] ProcessItem_Completes均声明为async void,需要改为async Task。其求值要求file-not-contains "async void"、output-matches "async Task"。
在最终结果中,必须说明源码变更的原因(xUnit.net v3 拒绝async void测试,因此每个受影响方法现在返回Task),而不是只报告机械替换;同时说明精确包版本是如何从配置源解析的。
Step 7:处理属性的类型化破坏性变更(按需)
xUnit.net v3 中部分属性从"两个字符串(完全限定类型名 + 程序集名)"改为接受System.Type。受影响属性:
CollectionBehaviorAttributeTestCaseOrdererAttributeTestCollectionOrdererAttributeTestFrameworkAttribute
例如,[assembly: CollectionBehavior("MyNamespace.MyCollectionFactory", "MyAssembly")]必须转换为[assembly: CollectionBehavior(typeof(MyNamespace.MyCollectionFactory))]。
夹具 convert-string-based-attribute-constructors-to-typeof-syntax 的OrderedTests.cs使用了[TestCaseOrderer("MyApp.Tests.AlphabeticalOrderer", "TestProject")],需要转换为typeof()语法;对应求值还要求CollectionBehavior不再以字符串构造,并最终通过dotnet test -p:TreatWarningsAsErrors=true。若类型化属性指向自定义类型(如 collection factory),还需同步迁移被引用类型的 v3 契约——例如实现 xUnit v3 的IXunitTestCollectionFactory行为,仅让属性编译通过而留下空工厂不算完整迁移。
Step 8:自定义 Fact/Theory 属性的源码信息(按需)
识别所有继承自FactAttribute或TheoryAttribute的自定义属性。v3 要求这些属性提供源码信息。例如:
internal sealed class MyFactAttribute : FactAttribute { public MyFactAttribute() { } }必须改为:
internal sealed class MyFactAttribute : FactAttribute { public MyFactAttribute( [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = -1 ) : base(sourceFilePath, sourceLineNumber) { } }夹具 update-custom-factattribute-to-include-source-information-pa 的CustomAttributes.cs中,RetryFactAttribute(继承FactAttribute)与ConditionalTheoryAttribute(继承TheoryAttribute)都只有无参构造,需要按上述模式补充[CallerFilePath]与[CallerLineNumber]参数并转发给base()。
报告完成前,必须读回每个受影响的FactAttribute/TheoryAttribute派生构造器,逐个命名类型,确认 caller-info 参数与对应的base(sourceFilePath, sourceLineNumber)转发都已存在。仅测试运行通过不能证明源码信息已被正确传播。
Step 9:继承BeforeAfterTestAttribute的签名更新(按需)
识别所有继承自BeforeAfterTestAttribute的自定义属性。v3 改变了方法签名:之前Before/After的 override 长这样:
public override void Before(MethodInfo methodUnderTest) { // 自定义逻辑 base.Before(methodUnderTest); // 自定义逻辑 } public override void After(MethodInfo methodUnderTest) { // 自定义逻辑 base.After(methodUnderTest); // 自定义逻辑 }必须改为:
public override void Before(MethodInfo methodUnderTest, IXunitTest test) { // 自定义逻辑 base.Before(methodUnderTest, test); // 自定义逻辑 } public override void After(MethodInfo methodUnderTest, IXunitTest test) { // 自定义逻辑 base.After(methodUnderTest, test); }保持BeforeAfterTestAttribute基类、保留 override 修饰符、保留现有 base 调用及其相对于自定义逻辑的顺序。直接实现IBeforeAfterTestAttribute接口虽然能编译,但这不是机械的 v2→v3 迁移,且可能丢弃基类行为。
夹具 update-beforeaftertestattribute-overrides-with-ixunittest-pa 的DatabaseSetupAttribute.cs正是 v2 形态:Before(MethodInfo)/After(MethodInfo)。其求值断言文件保留: BeforeAfterTestAttribute、出现IXunitTest、且base.Before(与base.After(均存在。报告完成前,应读出属性文件并引用实际的Before(MethodInfo, IXunitTest)与After(MethodInfo, IXunitTest)签名,明确确认base.Before与base.After收到同一个IXunitTest参数——仅笼统声称"已更新 override"是证据不足的。
Step 10:处理新的 xUnit 分析器警告(按需)
xunit.v3 引入了新的分析器警告,最典型的是xUnit1051:对接受CancellationToken的方法使用TestContext.Current.CancellationToken。若项目中出现此类警告,应一并处理。
Step 11:迁移Xunit.SkippableFact(按需)
若项目引用了Xunit.SkippableFact包,彻底移除该包引用,然后消除来自该包的 API 用法:
- 将
SkippableFact属性改为常规Fact; - 将
SkippableTheory属性改为常规Theory; - 将
Skip.If调用改为Assert.SkipWhen; - 将
Skip.IfNot调用改为Assert.SkipUnless。
夹具 migrate-xunit-skippablefact-to-xunit-net-v3-built-in-skip-ap 的ConditionalTests.cs展示了典型用法:[SkippableFact]方法内Skip.IfNot(OperatingSystem.IsWindows()),[SkippableTheory]方法内Skip.If(...)。其求值要求输出匹配Assert.SkipWhen|Assert.SkipUnless、不匹配SkippableFact...Version,并且不创建新的Directory.Build.props(用test ! -e Directory.Build.props验证)。
当夹具允许时,验证两条分支:默认条件应报告预期的跳过原因,启用条件应执行并通过——仅靠 grep 加一次普通通过运行,无法证明运行时跳过语义被保留。此转换限定在既有项目/中心包文件与包含这些 API 的源文件中;不要仅为完成配套包迁移而新建Directory.Build.props;任何必需的运行器属性在无共享 props 文件时都应放进既有测试项目。
Step 12:更新配套包(按需)
- 从配置源查询相互兼容的集合,而不是独立解析每个包:
Xunit.Combinatorial1.x 应升级到 2.x 或更高,Xunit.StaFact1.x 应升级到与所选xunit.v3主版本兼容的版本线; - 不要依据产品名或主版本号相同来推断配套包兼容性。应使用包依赖约束与配置源上实际可用的版本,并通过测试发现证明所选集合有效;
- 切换到可执行输出后,从 Linux/macOS 构建
net*-windows项目时,若需要跨目标平台,设置EnableWindowsTargeting=true; - 运行测试,将预期的平台跳过(如 Linux 上的 STA 测试)与失败区分开来确认。
夹具 update-xunit-combinatorial-and-xunit-stafact-companion-packa 对应此场景,其求值要求Xunit.Combinatorial从 1.x 升级到兼容的 2.x、Xunit.StaFact升级到配置源上兼容所选 xunit.v3 主版本的稳定版本,并同步迁移核心xunit包、设置OutputType=Exe。
Step 13:构建并验证
构建解决方案并修复所有剩余编译错误,然后运行dotnet test确认所有测试以与迁移前相同的结果通过。仓库的求值体系(eval.yaml)对几乎每个场景都以dotnet test -p:TreatWarningsAsErrors=true且退出码为 0 作为最终裁决条件——这从侧面印证了"无测试发现的构建不算成功"的完成契约:迁移的终点不是编译通过,而是测试被真正发现、执行并通过。
从源码结构看:技能与求值的对应关系
本技能隶属于dotnet-test-migration插件(plugin.json),其配套求值目录 tests/dotnet-test-migration/migrate-xunit-to-xunit-v3 为文档中的每一步提供了可执行的验证夹具:
- 基础迁移、CPM 迁移、YTest.MTP shim 迁移覆盖 Step 2~5 的包与运行器配置;
convert-async-void-test-methods-to-async-task覆盖 Step 6;convert-string-based-attribute-constructors-to-typeof-syntax覆盖 Step 7;update-custom-factattribute-to-include-source-information-pa覆盖 Step 8;update-beforeaftertestattribute-overrides-with-ixunittest-pa覆盖 Step 9;migrate-xunit-skippablefact-to-xunit-net-v3-built-in-skip-ap覆盖 Step 11;update-xunit-combinatorial-and-xunit-stafact-companion-packa覆盖 Step 12;detect-incompatible-target-framework-and-stop-migration验证 Step 1 的硬性停止条件;recognize-project-already-on-xunit-net-v3-no-migration-neede验证决策矩阵中的"已迁移即停止"分支——其求值通过 diff 基线文件确认迁移过程未做任何多余修改。
可见,本技能的 13 步工作流、决策矩阵与完成契约,均可在 tests/dotnet-test-migration/migrate-xunit-to-xunit-v3 中找到一一对应的可运行验证,这为读者复现与自测迁移结果提供了现成的参考基线。
小结
xUnit.net v3 迁移本质上是一套"先识别、再分层处理"的确定性流程:先验证目标框架与包形态(Step 1),再做包映射与OutputType=Exe(Step 2~3),随后按原运行器配置保留 VSTest 或 MTP(Step 4),最后按代码模式逐项处理async void、类型化属性、自定义属性、分析器警告与配套包(Step 5~12)。每完成一步,都以"读回文件 + 运行测试发现"而非"仅编译通过"作为验证标准,最终交付一个包兼容集可溯源、测试结果与迁移前一致的解决方案。
【免费下载链接】skillsRepository for skills to assist AI coding agents with .NET and C#项目地址: https://gitcode.com/GitHub_Trending/skills17/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考