从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个主要难点:
-
异步测试并行限制:xUnit允许限制线程数,但不允许限制并发测试数,因为对于异步测试,这两者不同。使用TUnit,你可以明确选择并行运行的测试数。
-
设置和拆卸:xUnit主要依赖构造函数和IDisposable来处理测试设置和关闭。如果需要异步设置和拆卸,必须实现IAsyncLifetime,并且在继承方面存在一些复杂性。使用TUnit,如果需要,可以使用所有这些方法,但你还有一个额外的选项,[BeforeTest]和[AfterTest]属性。
-
程序集级别钩子:xUnit没有简单的方法在执行程序集的测试之前运行某些东西,TUnit有[BeforeAssembly]和[AfterAssembly]。
-
TestContext:xUnit不在拆卸方法等中跟踪"当前"测试的状态。使用TUnit,你可以将上下文对象注入拆卸方法或使用静态TestContext.Current。
-
断言: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包添加到测试项目中,例如:
|
|
此时,项目中既有xUnit也有TUnit引用。
2. 删除自动添加的全局using
TUnit向项目添加隐式全局using语句,但是同时拥有这些和xUnit using语句可能会导致一些命名空间冲突。下一步重要的是项目构建成功,因此在测试项目中禁用using语句:
|
|
现在应该能够构建项目,这也将确保所有TUnit分析器正在运行并可用。
3. 将xUnit用法转换为TUnit
下一步是真正巧妙的部分。TUnit包括一个分析器,帮助从xUnit转换为TUnit。最棒的是,你可以从命令行运行分析器和相关的代码修复程序,它将为你将项目转换为TUnit:
|
|
我在库中的两个测试项目上运行了这个命令,它完成了所有繁重的转换工作。它删除了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阶段进行以下更改:
|
|
我遇到的最后一个问题是我使用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看起来非常有趣,我将有兴趣地关注它,并可能转换更多我的项目!