Fluent Assertions 要付費了,該怎麼辦呢?

其實這也不是新聞了,早在今年 1/15 時大家就已經知道並且被商業授權的費用給嚇了一大跳(每個人 $129.95 USD)。

現在特地寫一篇是因為上週因為 AutoMapper 將要變成商業化授權而寫了兩篇文章介紹替代套件,就想到好像並沒有對於 Fluent Assertions 轉變為商業授權去寫一篇文章與替代方案,於是就以 Fluent Assertions 從 v8.0.0 起轉為商業授權,簡單寫了這篇去說明授權限制、專案應對策略,以及替代的 Assertion Library,提供給大家做個參考。

Fluent Assertions

約十年前我在 Blogger 那邊就寫了一篇文章介紹這個套件,那時候 Fluent Assertions 是 4.0.1 版,也是我這十年年寫測試時必裝的套件之一。

然後時間來到今年 (2025) 的 1/15 這天,就看到了 Nick Chapsas 的這個影片「Stop Using FluentAssertions Now」時讓我驚訝不已

Will 保哥也在 Facebook  社團「台灣 .NET 技術愛好者俱樂部」發表了一篇貼文,並且提供了一則總結

對於沒有在寫測試的開發者來說並不會有什麼影響,但對於許多有在寫測試的 .NET 開發人員與團隊來說,這是個相當不得了的事件。其實很多開源軟體、套件也從最初的開源且為不需付費購買授權開始,當使用的人越來越多,提出 issues 也逐漸增加,隨之而來的就是要投入更多的開發與維護成本,當贊助以及開發者或維護團隊難以平衡開發維護成本時就會轉為商業化。

商業化也不是一件壞事,因為有收錢就表示會持續的營運、開發和維護,只不過 Fluent Assertions 改為 Exceed 之後每個人每年要花費 $129.95 USD (折合台幣約 4,200 元以上) 購買使用授權。Rider Commercial  一年的授權費用是 $149 USD,如果買的是 dotUltimate 方案一年的授權費用是 $169 USD,經過比較後就會覺得 Fluent Assertions 這個收費真的是很貴。

相關連結:

 

Fluent Assertions 商業化相關資訊

是從哪一個版本開始?

v8.0.0:改為商業授權

  • 新版 FluentAssertions(8.x、9.x…)都需付費授權才能用於商業開發或測試

v7.x(含更早)

  • 仍維持 Apache 2.0 開源許可,免費且持續提供安全性/效能更新

商業授權限制與定價

  1. 授權方式
    按人頭計費:使用者(開發、測試人員)皆需擁有授權。
    不可轉讓/共用:一人一授權。
  2. 訂閱週期
    一律 12 個月,無部分退費機制
  3. 主要方案與價格
小型企業的年營收需低於一百萬美金
標準授權就是一個人需付 $129.95 USD

 

如何在既有專案裡將 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檔案

首先,在 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=&quot;[7.2.0]&quot;,請修正 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

  1. 在方案上點擊滑鼠右鍵,選擇「加入」>「新增項目」
  2. 選擇Directory.Build.targets檔案
  3. 在方案總管裡的「方案項目」裡就可以看到Directory.Build.targets檔案
在方案上點擊滑鼠右鍵,選擇「加入」>「新增項目」
選擇 Directory.Build.targets 檔案
在方案總管裡的「方案項目」裡就可以看到 Directory.Build.targets 檔案

Rider

如果你和我一樣是使用 Rider 的話,就依照以下的方式將Directory.Build.targets檔案給加入為方案項目

在 Solution  點擊滑鼠右鍵 > Add > New Solution Folder
建立「方案項目」的方案資料夾
在「方案項目」點擊滑鼠右鍵 > Add > Existing Item…
選擇 Directory.Build.targets 檔案
檔案加入到「方案項目」裡

以上就是我目前所覺得符合防止「人為手動更新特定套件版本」的方式,各位或許還有更好的方式,請務必跟我說,謝謝。

 

改用替代方案「AwesomeAssertions」

其實之前除了 Fluent Assertsions 外,還有很多人選用 Shouldly 這個套件,一直沒有在專案裡使用過,直到得知 Fluent Assertsions 於版本 8.x 之後要買授權後,我將手邊測試專案改用 Shouldly。是可以達到同樣驗證的目的,但是很多 assertions 語法的使用和擴展性、豐富性等等都不及 Fluent Assertions,畢竟也用了快十年,要短時間裡去適應另外一個套件是很困難。

Shouldly

直到後來得知原來有個從 Fluent Assertions 分支出去的另一個版本叫做「AwesomeAssertsions」,於是我直接果斷改用 AwesomeAssertsions

AwesomeAssertsions

AwesomeAssertions 是由社群維護的一個 FluentAssertions 分支(fork),目的是保留原本在 v7.x 及 v8-rc2 之前的所有功能與 API,並持續以 Apache 2.0 許可免費提供給所有用戶使用,避免商業授權衍生的限制與費用。適合希望完全免費、避開商業授權限制,又需維持 FluentAssertions 原有 API 與持續修補的專案使用。

核心特色

  • 永遠維持 Apache 2.0,不會改為 MIT 或商業授權
  • 主動從 FluentAssertions v7.x cherry‑pick 重要修補,並合併社群 PR
  • 仍沿用 FluentAssertions namespace(未來可能考慮改名
  • 任何基於 v8-rc2 之前的 commits 都可合法回溯使用,不受後續商業政策影響
命名空間是 API 的一部分,是在 Apache 2.0 許可下開發的。 Google v. Oracle 案裁定 API 被視為合理使用,因此在 API 類別名稱中包含「FluentAssertions」命名空間是可以接受的。雖然現在是允許的,但我們可能會考慮在將來更改命名空間。

安裝與替換方式

如果你真的下定決心要從 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 也需要做更改

將 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 之後,做了很多的調整:

簡單來說,就是短時間內這個套件應該不會有更新,其他的替代套件更改也會花比較多的時間而且會需要很大的調整(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。

以上

 

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力