其实这也不是新闻了,早在今年 1/15 时大家就已经知道并且被商业授权的费用给吓了一大跳(每个人 $129.95 USD)。
现在特地写一篇是因为上週因为 AutoMapper 将要变成商业化授权而写了两篇文章介绍替代套件,就想到好像并没有对于 Fluent Assertions 转变为商业授权去写一篇文章与替代方案,于是就以 Fluent Assertions 从 v8.0.0 起转为商业授权,简单写了这篇去说明授权限制、专案应对策略,以及替代的 Assertion Library,提供给大家做个参考。
Fluent Assertions
约十年前我在 Blogger 那边就写了一篇文章介绍这个套件,那时候 Fluent Assertions 是 4.0.1 版,也是我这十年年写测试时必装的套件之一。
- 编写单元测试时的好用辅助套件 - Fluent Assertions
然后时间来到今年 (2025) 的 1/15 这天,就看到了 Nick Chapsas 的这个影片「Stop Using FluentAssertions Now」时让我惊讶不已
Will 保哥也在 Facebook 社团「台湾 .NET 技术爱好者俱乐部」发表了一篇贴文,并且提供了一则总结
- https://www.facebook.com/groups/DotNetUserGroupTaiwan/posts/3579332135693212/
- https://felo.ai/search/ic5vdfUNFDh2wMBZTMs4Xf
对于没有在写测试的开发者来说并不会有什么影响,但对于许多有在写测试的 .NET 开发人员与团队来说,这是个相当不得了的事件。其实很多开源软体、套件也从最初的开源且为不需付费购买授权开始,当使用的人越来越多,提出 issues 也逐渐增加,随之而来的就是要投入更多的开发与维护成本,当赞助以及开发者或维护团队难以平衡开发维护成本时就会转为商业化。
商业化也不是一件坏事,因为有收钱就表示会持续的营运、开发和维护,只不过 Fluent Assertions 改为 Exceed 之后每个人每年要花费 $129.95 USD (折合台币约 4,200 元以上) 购买使用授权。Rider Commercial 一年的授权费用是 $149 USD,如果买的是 dotUltimate 方案一年的授权费用是 $169 USD,经过比较后就会觉得 Fluent Assertions 这个收费真的是很贵。
相关连结:
- https://github.com/fluentassertions/fluentassertions
- https://fluentassertions.com/
- https://xceed.com/products/unit-testing/fluent-assertions/
- https://github.com/fluentassertions/fluentassertions/pull/2943
- FluentAssertions becomes paid software for commercial use | reddit
Fluent Assertions 商业化相关资讯
是从哪一个版本开始?
v8.0.0:改为商业授权
- 新版 FluentAssertions(8.x、9.x…)都需付费授权才能用于商业开发或测试
v7.x(含更早)
- 仍维持 Apache 2.0 开源许可,免费且持续提供安全性/效能更新
商业授权限制与定价
- 授权方式
按人头计费:使用者(开发、测试人员)皆需拥有授权。
不可转让/共用:一人一授权。 - 订阅週期
一律 12 个月,无部分退费机制 - 主要方案与价格
如何在既有专案里将 Fluent Assertions 锁定在 7.x 版本呢?
若专案已使用 FluentAssertions 7.x,且不想升级至付费版,可在 csproj 中固定版本:
<PackageReference Include="FluentAssertions" Version="[7.0.0]" />
如此 NuGet 将只还原 7.0.0,不会自动拉取 8.x 以上版本,同时仍可获得官方释出的安全与重要更新。
但这只是在于避免 packages restore 时不会因为 PackageReference 没有设定 Version 而去拉取最新的版本。
下面是在 VIsual Studio 2022 的 NuGet 套件管理员所显示的画面,虽然 FluentAssertsion 套件显示目前版本为 7.2.0 且最高版本为 8.2.0,但是在右边介面里还是可以选择安装 8.2.0 版本
如果是使用 Rider 的话,既使在 csproj 里去固定版本,开启 NuGet Packages 管理介面还是会提示要更新升级到 8.2.0 版本
所以这个锁定版本号是在防止 pacakge restore 时因为没有指定版本而回复到目前最新且需要付费的版本。
而这时候有人没有注意就去按下更新,还是会让版本直接更新到 8.2.0,所以还是要小心。
避免有人手动将 Fluent Assertions 的版本
为了有效地防止人为手动去更新 Fluent Assertions 版本,尝试了很多的方式,像是版本範围设定、使用 packages.lock.json 或 Directory.Packages.props (CPM: Central Package Management 中央版本管理),但都不太管用。
我所要防堵的是人为手动更新的这个行为,因为团队里总是有人会「好心地」去更新套件的版本,只要套件版本更新而且重新建置成功、所有测试都通过了就会觉得完成一件功德,却忽略了有些套件的版本更新是会有授权的问题。
在与 ChatGPT 来来回回好多次之后,确定我所要做的就是「防堵人为手动更新」的这个行为,所以在进行建置或重建专案时就去检查各个专案的 csproj 档案,检查 Fluent Assertions 的 PackReference 的 Version 值是否为[7.2.0]
,只要不是指定的字串就专案建置错误并显示错误讯息。
Directory.Build.targets
这里所用到的是Directory.Build.targets
档案
- MSBuild .targets 档案 - MSBuild | Microsoft Learn
- 使用扩充勾点自订组建 - MSBuild | Microsoft Learn
- 依资料夹或解决方案自订您的构建 - MSBuild | Microsoft Learn
首先,在 Solution 根目录下(.sln 档案同个路径)建立Directory.Build.targets
接着开启Directory.Build.targets
档案,将以下内容複製贴上
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="EnforceFluentAssertionsExact"
BeforeTargets="Build">
<!--
找出所有 FluentAssertions,且 Version 不等于 "[7.2.0]" 的引用
-->
<ItemGroup>
<WrongFA Include="@(PackageReference)"
Condition="
'%(Identity)' == 'FluentAssertions'
and
'%(Version)' != '[7.2.0]'
" />
</ItemGroup>
<!-- 若有任何一笔,就错误中断,并提示实际写的版本 -->
<Error Condition=" '@(WrongFA)' != '' "
Text="❌ FluentAssertions 版本 (%(WrongFA.Version)) 必须写成 Version="[7.2.0]",请修正 csproj。" />
</Target>
</Project>
然后重新载入 Solution 或重新开启 Visual Studio 2022 或 Rider
确认每个有使用到 Fluent Assertions 套件的专案 csproj 档案里是否都为以下的内容
<PackageReference Include="FluentAssertions" Version="[7.2.0]" />
如果有一个专案里 FluentAssertions 的 PackageReference Version 不是[7.2.0]
,执行专案或方案建置时就会出现错误。
例如我将 Sample.ServiceTests.csproj 里的 Fluent Assertions 改为 8.2.0
那么建置时就会发生错误,并显示错误讯息
而在修正回[7.2.0]
后再重新建置就不会出现错误。
之后如果 Fluent Assertions 在 7.x 还有发布错误修复的新版本,就去修改Directory.Build.targets
档案里的 Fluent Assertions 为新的版本号,重新建置方案后就会经由建置的错误讯息得知有哪几个专案要做调整。
因为不用刻意将Directory.Build.targets
档案加入至.sln
档案里,所以很容易会遗忘它(务必要将这个档案加入到版本控管),这边可以透过两种方式将Directory.Build.targets
档案加入为方案项目,这样就可以在方案总管里看到档案,而且也方便之后的修改编辑。
Visual Studio
- 在方案上点击滑鼠右键,选择「加入」>「新增项目」
- 选择
Directory.Build.targets
档案 - 在方案总管里的「方案项目」里就可以看到
Directory.Build.targets
档案
Rider
如果你和我一样是使用 Rider 的话,就依照以下的方式将Directory.Build.targets档案给加入为方案项目
- Extend and organize your solution | JetBrains Rider Documentation
以上就是我目前所觉得符合防止「人为手动更新特定套件版本」的方式,各位或许还有更好的方式,请务必跟我说,谢谢。
改用替代方案「AwesomeAssertions」
其实之前除了 Fluent Assertsions 外,还有很多人选用 Shouldly 这个套件,一直没有在专案里使用过,直到得知 Fluent Assertsions 于版本 8.x 之后要买授权后,我将手边测试专案改用 Shouldly。是可以达到同样验证的目的,但是很多 assertions 语法的使用和扩展性、丰富性等等都不及 Fluent Assertions,毕竟也用了快十年,要短时间里去适应另外一个套件是很困难。
Shouldly
- https://github.com/shouldly/shouldly
- https://docs.shouldly.org/
直到后来得知原来有个从 Fluent Assertions 分支出去的另一个版本叫做「AwesomeAssertsions」,于是我直接果断改用 AwesomeAssertsions
AwesomeAssertsions
AwesomeAssertions 是由社群维护的一个 FluentAssertions 分支(fork),目的是保留原本在 v7.x 及 v8-rc2 之前的所有功能与 API,并持续以 Apache 2.0 许可免费提供给所有用户使用,避免商业授权衍生的限制与费用。适合希望完全免费、避开商业授权限制,又需维持 FluentAssertions 原有 API 与持续修补的专案使用。
- https://github.com/AwesomeAssertions/AwesomeAssertions
- https://awesomeassertions.org/
核心特色
- 永远维持 Apache 2.0,不会改为 MIT 或商业授权
- 主动从 FluentAssertions v7.x cherry‑pick 重要修补,并合併社群 PR
- 仍沿用 FluentAssertions namespace(未来可考虑改名)
- 任何基于 v8-rc2 之前的 commits 都可合法回溯使用,不受后续商业政策影响
安装与替换方式
如果你真的下定决心要从 Fluent Assertions 改为 AwesomeAssertions,那么前面所讲的预防人为手动更新版本的那些操作、修改与档案都可以移除(不过一样的作法之后可以用在 AutoMapper 与 MediatR 上)
NuGet 安装
Install-Package AwesomeAssertions
将专案里的 FluentAssertions 都移除安装,刚才加上去的方案项目与档案也都可以删掉,然后直接在测试专案安装 AwesomeAssertions
因为从 Fluent Assertions 7.2.0 更换到 AwesomeAssertions 8.1.0,建置出现了错误「Error CS0103 : 名称 'AssertionOptions' 不存在于目前的内容中」
在 FluentAssertions 8.x 中,已经移除了旧有的 AssertionOptions 静态类别,现在要更换为「AssertionConfiguration」(FluentAssertions 与 AwesomeAssertions 都一样)
AwesomeAssertions 的 Namespace 命名空间
现阶段 AwesomeAssertsion 的 namespace 没有更换是蛮重要的,因为短时间内更换套件就不需要去修改程式码与专案,而且许多的断言方法都还是一模一样的使用(因为是直接分支出来),所以很多习惯的语法与设定就可以直接沿用。
虽然短时间内一些原本 Fluent Assertsions 生态所发展的一些扩展套件,例如:FluentAssertion.Web,两边都还会支援,但之后如果 AwesomeAssertions 被要求更改命名空间后,到时又会有一番的变动了,反正到时候在说。
FluentAssertions.Web
上面介绍到 FluentAssertions.Web,接着就在 Sample.WebApplicationIntegrationTests 整合测试专案里出现了以下的错误
Sample.WebApplicationIntegrationTests 整合测试专案里有安装使用「FluentAssertions.Web」,但因为已经把 FluentAssertions 7.2.0 改为 AwesomeAssertions 8.1.0,所以 FluentAssertions/Web 也需要做更改
- https://github.com/adrianiftode/FluentAssertions.Web
将 Sample.WebApplicationIntegrationTests 整合测试专案里的 FluentAssertions.Web 1.8.0 移除,然后改为安装 FluentAssertions.Web.v8 (1.8.0)
更新完成后再重新建置方案,就不会再出现错误了
Fluent Assertions for ASP.NET Core MVC
这是一套基于 Fluent Assertions 所发展的一个套件,主要是用于 ASP.NET Core MVC or WebApi 单元测试专案里验证 Action 方法的执行结果,例如以下的测试方法的验证,可以用比较流畅且简单的语法做验证。
这个套件也已经多年没有更新的,4.2.0 这个版本也三年多没有再更新了,在还是使用 FluentAssertsion 7.2.0 的时候还不会出现执行错误,但无论是使用 FluentAssertsions 8.2.0 或 AwesomeAssertsions 8.1.0 之后的执行都会出现错误
FluentAssertions 更新到 8.x 后,`fluentassertions.aspnetcore.mvc` 中的所有 assertion 类别 constructor 都只呼叫了 `base(subject)`,而 8.x 的 `ObjectAssertions` 已移除了单一参数的建构函式,改为需要同时传入 `AssertionChain`,因此执行时会出现:System.MissingMethodException: Method not found: 'Void FluentAssertions.Primitives.ObjectAssertions..ctor(System.Object)'
错误
从 FluentAssertions 7 升级到 8 之后,做了很多的调整:
- https://fluentassertions.com/upgradingtov8
简单来说,就是短时间内这个套件应该不会有更新,其他的替代套件更改也会花比较多的时间而且会需要很大的调整(ex: MyTested.AspNetCore.Mvc),所以我会建议就直接捨弃 FluentAssertions.AspNetCore.MVC,assertion 的部分就改回使用 FluentAssertsions 的验证语法。
例如以下是原本使用 FluentAssertsions.AspNetCore.MVC 写法
[Theory]
[AutoDataWithCustomization]
public async Task GetAllAsync_从service取得的资料为空集合_应回传OkObjectResult及空的资料集合(
[Frozen] IShipperService shipperService,
ShipperController sut)
{
// arrange
shipperService.GetAllAsync().Returns([]);
// act
var actual = await sut.GetAllAsync();
// assert
actual.Should().BeOkObjectResult()
.WithStatusCode(200)
.WithValueMatch<IEnumerable<ShipperOutputModel>>(x => !x.Any());
}
改回使用内建的 FluentAssertions Type Assertion
[Theory]
[AutoDataWithCustomization]
public async Task GetAllAsync_从service取得的资料为空集合_应回传OkObjectResult及空的资料集合(
[Frozen] IShipperService shipperService,
ShipperController sut)
{
// arrange
shipperService.GetAllAsync().Returns([]);
// act
var actual = await sut.GetAllAsync();
// assert
var okObjectResult = actual.Should().BeOfType<OkObjectResult>().Which;
okObjectResult.StatusCode.Should().Be(200);
var model = okObjectResult.Value.Should().BeAssignableTo<IEnumerable<ShipperOutputModel>>().Which;
model.Should().BeEmpty();
}
- BeOfType<T>():断言结果的具体型别,并回传一个 ObjectAssertions<T>,透过 .Which 拿到强型别物件。
适用于 - 当你需要严格检查「完全相同的类别」 - BeAssignableTo<T>():断言 Value 可以转成你要的型别,并用 .Which 拿到转型后的值。
适用于 - 当你只在意物件「符合某个基底类别或介面」
一想到有一堆的测试案例要着手进行改写,想到就觉得烦,以后要慎选套件呀…
最后
没想到写着写着就一大堆…
FluentAssertions 的商业化,带来了更明确的商业授权保护与支援,但也增加了团队的採用成本。选择最适合的策略,才能在「功能需求」与「预算限制」间取得平衡。
不过我想应该只有为数不多的人或公司企业会去购买商业授权,尤其是台湾 .NET 开发圈的开发者、团队和公司企业,会写测试的已经不多了,会再去购买授权的又会更少。所以就以目前的状况来看,可以的话就尽快将手边还有在使用 Fluent Assertsions 的专案赶快改用 AwesomeAssertions 或 Shouldly。
以上
纯粹是在写兴趣的,用写程式、写文章来抒解工作压力