从xUnit迁移到TUnit:新一代.NET测试框架实战指南

本文详细介绍了如何将xUnit测试项目迁移到新兴的TUnit测试框架,包括TUnit的核心特性、与xUnit的技术差异、具体迁移步骤及遇到的问题解决方案。

从xUnit测试项目迁移到TUnit

什么是TUnit

TUnit是C#的下一代测试框架,通过源生成测试、默认并行执行和Native AOT支持,超越了传统框架。基于现代的Microsoft.Testing.Platform构建,TUnit提供更快的测试运行、更好的开发体验和无与伦比的灵活性。

该项目仍相对较新,首个alpha版本于2024年出现,尚未发布v1.0.0。但根据当前状态,“API基本稳定”,从功能角度以及文档和指南来看都相当完善。

另一个重要方面是它使用较新的Microsoft.Testing.Platform体验(而非旧的VSTest运行程序),因此需要使用较新版本的.NET SDK。尽管它是新的,Visual Studio、VS Code和Rider都支持较新的轻量级平台,因此也支持TUnit(尽管可能需要在IDE中启用选项)。

一个需要50ms执行的测试,重复100次(包括生成新进程和初始化测试框架)

该场景的第一组基准测试在Apple M1 mac上运行,并显示出与其他框架相比非常令人印象深刻的速度提升(我认为这最终归因于TUnit使用的并行化增加):

方法 平均值 误差 标准差
TUnit AOT 235.8 ms 12.94 ms 38.15 ms
TUnit 643.5 ms 21.41 ms 63.13 ms
NUnit 13,517.6 ms 269.19 ms 644.96 ms
xUnit 13,932.4 ms 276.32 ms 505.26 ms
MSTest 13,704.8 ms 269.71 ms 614.28 ms

为什么要更改测试框架?

通常,我不建议更改测试框架。是的,不同框架之间存在差异,但许多差异只是表面上的;为了微小的更改而麻烦值得吗?你能获得多少收益?

然而,我还是考虑了这一点😅这实际上是由我在工作中发现的一个问题驱动的,当时我们在处理DataDog .NET Tracer,尝试更新到使用.NET 10预览版5 SDK时,我们的xUnit测试库无法发现任何测试。不是失败,只是无法运行任何测试😬。

这个问题最终是一个无关的干扰,但它让我们思考;我们在某个时候遇到xUnit问题并完全卡住的几率有多大?如果不是因为"xUnit v3"问题,这可能看起来有点不合理……

xUnit v2使用"xUnit" NuGet包,是大多数人在谈论xUnit时想到的框架(至少历史上如此)。xUnit v2基本上支持所有目标框架;它支持.NET standard 1.1+(因此所有.NET Core)和.NET Framework 4.5.2+。

xUnit.v3在某些方面是演进,在其他方面是xUnit的重建。它使用不同的NuGet包,仅支持.NET Framework 4.7.2和.NET 8+目标框架。这显然完全由维护者决定支持什么,他们已决定仅支持Microsoft支持的框架,从表面上看这完全合理。

但这对我们在工作中提出了一个棘手的问题,因为我们需要在客户使用的各种旧框架上进行测试。即使在我的OSS项目中,我通常也支持更多目标框架以便消费者易于使用,而且这个范围总是比xUnit.v3的矩阵大。

所有这些意味着我不太可能采用xUnit.v3,无论是在工作中还是在我的个人项目中。这意味着我基本上永远停留在xUnit v2上。这让我感到紧张,因为xUnit v2不再接收更新……

我对xUnit还有其他一些小问题,这也增加了不安感,尤其是xUnit.v3在6个月内发布了3个主要版本。这种级别的变动我个人不想在测试框架中处理。

所有这些让我想知道TUnit是否是一个潜在的替代方案,因此我决定在我的一个OSS项目中尝试它。

TUnit与xUnit有何不同?

TUnit文档的一个优点是它们明确尝试解决TUnit与其他测试框架之间的差异。对于xUnit,TUnit文档突出了5个主要难点:

  1. 异步测试并行限制:xUnit允许限制线程数,但不允许限制并发测试数,因为对于异步测试,这两者不同。使用TUnit,你可以明确选择并行运行的测试数。

  2. 设置和拆卸:xUnit主要依赖构造函数和IDisposable来处理测试设置和关闭。如果需要异步设置和拆卸,必须实现IAsyncLifetime,并且在继承方面存在一些复杂性。使用TUnit,如果需要,可以使用所有这些方法,但你还有一个额外的选项,[BeforeTest]和[AfterTest]属性。

  3. 程序集级别钩子:xUnit没有简单的方法在执行程序集的测试之前运行某些东西,TUnit有[BeforeAssembly]和[AfterAssembly]。

  4. TestContext:xUnit不在拆卸方法等中跟踪"当前"测试的状态。使用TUnit,你可以将上下文对象注入拆卸方法或使用静态TestContext.Current。

  5. 断言:xUnit断言使用某种"传统"的Assert.Equal(x, y)格式,这使得很难知道x或y是实际值还是期望值。TUnit断言使用流畅风格,如FluentAssertions或Shouldly。

公平地说,xUnit,这些抱怨中的许多是针对xUnit的v2版本。例如,xUnit v3有一个类似于TUnit的TestContext类型,以及一个[AssemblyFixture]属性用于提供程序集级别钩子。

也就是说,TUnit还带来了一系列额外功能:

  • 源生成:TUnit不依赖运行时发现测试,而是依赖源生成器来驱动测试发现,这使得运行时速度更快。
  • Native AOT支持:使用Native AOT的一个好处是,你实际上可以使用Native AOT发布测试项目。对于大多数项目来说,这可能有些过分,但如果你要测试一个打算使用NativeAOT部署的应用程序,这可能很方便。
  • 测试排序:TUnit提供了一个[DependsOn]属性,允许对测试提供隐式排序,而无需一般禁用并行化或设置顺序Order值。
  • 控制台捕获:使用xUnit v2,写入Console的任何内容都不会在测试输出中捕获,但TUnit自动拦截这些日志并将其与当前测试关联。记录一下,xUnit v3也有此选项。

当然,从我的角度来看,真正重要的区别是支持的框架。正如我之前提到的,xUnit v3仅支持.NET Framework 4.7.2和.NET 8+。另一方面,TUnit支持.NET 8+,但也支持.NET Standard 2.0,这意味着.NET Framework 4.7.2,但更重要的是也支持.NET Core 2.0+,从我的角度来看,这是真正的杀手级功能。

因此,考虑到这一点,我开始尝试将我的一个OSS项目转换为使用TUnit而不是xUnit会是什么样子。

将xUnit项目转换为TUnit

TUnit项目值得进一步赞扬:他们有一个逐步指南描述从xUnit迁移到TUnit。而且它工作得非常好!因此,本节主要是这些步骤的重述,并应用于我的仓库的示例,以及一些注意事项。

1. 添加TUnit包

首先,将TUnit包添加到测试项目中,例如:

1
dotnet add package TUnit

此时,项目中既有xUnit也有TUnit引用。

2. 删除自动添加的全局using

TUnit向项目添加隐式全局using语句,但是同时拥有这些和xUnit using语句可能会导致一些命名空间冲突。下一步重要的是项目构建成功,因此在测试项目中禁用using语句:

1
2
3
4
<PropertyGroup>
    <TUnitImplicitUsings>false</TUnitImplicitUsings>
    <TUnitAssertionsImplicitUsings>false</TUnitAssertionsImplicitUsings>
</PropertyGroup>

现在应该能够构建项目,这也将确保所有TUnit分析器正在运行并可用。

3. 将xUnit用法转换为TUnit

下一步是真正巧妙的部分。TUnit包括一个分析器,帮助从xUnit转换为TUnit。最棒的是,你可以从命令行运行分析器和相关的代码修复程序,它将为你将项目转换为TUnit:

1
dotnet format analyzers --severity info --diagnostics TUXU0001

我在库中的两个测试项目上运行了这个命令,它完成了所有繁重的转换工作。它删除了using xunit,将[Fact]更改为[Test],将[InlineData]更改为[Arguments],以及其他各种更改:

这并不完全成功,因为它留下了一些xUnit断言未触及,但由于我通常无论如何都转换为FluentAssertions,我只是先完成该转换。

另一件要注意的事情是,分析器对一些行执行了一般重新格式化(例如,你可以在上面的屏幕截图中看到删除的空白行)。这对我来说不是一个大问题,但值得记住。

4. 恢复全局using

现在所有用法都已转换,需要重新启用TUnit命名空间,因此最简单的事情是恢复TUnitImplicitUsings和TUnitAssertionsImplicitUsings属性。

5. 删除不需要的包

我们快完成了。最后,你可以删除所有xUnit包,包括运行程序包,并且重要的是也删除Microsoft.NET.Test.Sdk包。TUnit使用较新的轻量级Microsoft.Testing.Platform包,并传递引用该包,因此你需要的唯一包是TUnit。

此时,你应该能够构建和测试项目,如果一切顺利,可以使用dotnet test测试项目,或你喜欢的任何方式!

相关问题

我遇到了一些小问题。第一个我已经提到,我必须修复一些未正确转换的Assert调用,主要是Assert.Throws<>调用。如前所述,我通过在转换为TUnit之前转换为FluentAssertions来解决这个问题。

我遇到的另一个问题是TRX报告生成。TUnit和Microsoft.Testing.Platform通过扩展包Microsoft.Testing.Extensions.TrxReport支持TRX报告生成。理论上,你只需要删除与Microsoft.NET.Test.Sdk一起使用的–logger trx调用,并添加Microsoft.Testing.Platform所需的–report-trx版本。

然而,我发现我需要在–参数后添加–report-trx(和–results-directory参数)。与此相关的是,我必须解决Nuke(我首选的构建系统)没有对Microsoft.Testing.Platform库提供一流支持的事实。

总而言之,我不得不对Nuke脚本中的Test阶段进行以下更改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DotNetTest(s => s
    .SetProjectFile(Solution)
    .SetConfiguration(Configuration)
    .SetProperty("Version", Version)
    .When(IsServerBuild, x => x
-       .SetLoggers("trx")
-       .SetResultsDirectory(TestResultsDirectory))
+       .SetProcessArgumentConfigurator(x=>x
+           .Add("--")
+           .Add("--report-trx")
+           .Add("--results-directory")
+           .Add(TestResultsDirectory)));

我遇到的最后一个问题是我使用Verify进行快照测试。Verify有多个测试框架的插件,包括TUnit。然而,不幸的是,对我来说,即使是最早版本的Verify.TUnit也只支持.NET 8+,不支持早期版本的.NET Core,这对我来说是一个阻碍。

Verify是另一个由于放弃旧框架而无限期停留在旧版本的库(在OSS和工作中都是)。加上对破坏性更改的某种激进态度,意味着我比最新版本落后约12-16个主要版本,并且无法升级😅

因此,这让我有点卡住。我考虑完全删除Verify,因为我只在一个测试中使用它。但我也想考虑将其他项目移动到TUnit,并且由于Verify依赖,转换这些项目最终可能很棘手……

所以我做了一些相当激烈的事情。我在可以添加.NET Core 2.1目标的最高版本(21.3.0)分叉了Verify,并上传了我自己的版本。我不设想这会进一步发展或必然成为长期解决方案,但它可能使我更容易将我的一些其他库转换为TUnit,而不必同时切换到不同的快照测试库!

总结

在本文中,我讨论了新的TUnit测试框架,查看了它的一些功能,以及它与其他框架的一些差异。然后我描述了xUnit创建xUnit.v3的举动如何促使我考虑将我的开源库的测试框架更改为TUnit。

这不是你应该轻率对待的事情,但xUnit.v3仅在.NET 8+(和.NET Framework)上工作的事实是我在库中使用它的障碍,因此我尝试了一个库的示例转换。

在文章的其余部分,我描述了将xUnit项目转换为TUnit的过程。我发现这非常顺利,这要归功于TUnit项目通过Roslyn分析器提供迁移工具。这个过程总体上工作顺利,转换后只需要我进行相对较小的更改。

总的来说,TUnit看起来非常有趣,我将有兴趣地关注它,并可能转换更多我的项目!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计