在 xunit 測試專案中使用依賴注入
Intro
之前寫過幾篇 xunit 依賴注入的文章,今天這篇文章將結合我在 .NET Conf 上的分享,更加系統的分享一下在測試中的應用案例。
之所以想分享這個話題是因為我覺得在我們開發過程中測試是非常重要的一部分,高質量專案的一個重要指標就是測試覆蓋率,同時依賴注入已經是一個現代化應用中不可缺少的一部分,我們的 .NET Core 也是從一開始就整合了依賴注入,依賴注入對於測試專案也是不能缺席的。
xunit 是 .net 裡目前使用的最多的測試元件,Xunit.DependencyInjection
是大師寫的一個 xunit 依賴注入的擴充套件,它是基於微軟的 GenericHost
(通用主機) 來實現的,使用它我們可以很輕鬆的實現依賴注入,很好的和 .NET Core 做整合。
How it works
那它是如何工作的呢?我們一起來看一下它的執行流程,它的執行流程分為四步
首先需要構建一個 Host,然後啟動這個 Host,啟動完成後執行測試用例,最後終止這個 Host
Host 又是如何構建的呢?我們一起看一下,Host 的構建也是分為四步
第一步,建立一個 HostBuilder
,大多數情況下我們不需要用這個方法,使用預設的實現就好
第二步,Host 配置,對 Host 做一些自定義配置
第三步,服務配置,註冊需要的服務
第四步,Configure
,可以做一些初始化的配置,比如配置初始化以及測試資料的初始化等
我們可以在測試專案裡建立一個 Startup
類來控制 Host
的構建過程
示例
接著我們來看一些實際的測試示例,示例分為三部分,首先是一些基本用法,然後是和其他元件的整合,最後是一些擴充套件用法
Get Started
首先來看一下 Startup
的用法,這個 Startup
和 asp.net core 裡的 Startup
是很像的,無論是使用方式上還是實現上都是類似的,有興趣的可以看一下原始碼對比一下,我們來看一下使用方式,通過下面的示例來感受一下
如果你只需要註冊服務,直接在 Startup
中新增一個 ConfigureServices
方法,在這個方法中註冊自己需要的服務即可,和 asp.net core 並無太多不同
如果你需要做一些初始化的工作,可以加一個 Configure
方法,在這個方法中實現自己的初始化邏輯就可以了,如果初始化的時候需要獲取注入的服務例項,直接作為方法引數就可以,類似於 asp.net core 中 Configure
方法,只是不需要配置 Http 請求管道
如果你需要使用的配置,需要使用 Configuration,可以在 ConfigureHost
方法中通過 ConfigureHostConfiguration
擴充套件方法註冊自己的配置
如果需要在註冊服務的時候用到配置,可以在 ConfigureServices
方法中新增一個 HostBuildContext
的引數,HostBuilderContext
中的 Configuration
物件就是在 ConfigureHost
中註冊的配置
如果需要在 Configure
方法中使用配置,直接新增一個 IConfiguration
的方法引數就可以了
我們再來看一下,如何在測試用例中使用注入的服務,一般情況下我們會直接通過構造器注入,在構造方法中新增需要注入的服務即可,除此之外我們還可以通過方法引數注入,結合 InlineData
和 MemeberData
使用,來看一下這個示例
IoC/AOP Integration
接著我們來看一下和其他元件的整合,AutoFac
是一個很流行的 IOC 元件,AspectCore
是檸檬大佬寫的一個 AOP 框架,我們以這兩個為例子來看一下如何整合第三方的依賴注入和 AOP 元件,前面我們已經提到它是基於微軟的 GenericHost
實現的,而 asp.net core 從 3.0 開始也是基於 GenericHost
實現的,所以在 asp.net core 裡怎麼整合,在這裡也是一樣的,來看一下示例,只需要使用對應的 ServiceProviderFactory
就可以了,是不是很簡單呢
Test Server Integration
然後我們來看一下如何和 TestServer
做整合,TestServer
主要用於整合測試,使用 TestServer
的好處在於它是基於記憶體進行互動的沒有真正的 HTTP 請求和 TCP 連結,會非常的高效,而且也不會監聽某一個埠,所以不會有埠許可權的問題。
TestServer
的使用主要有兩步,首先是服務的註冊,可以使用 IHostBuilder
或 IWebHostBuilder
的 UseTestServer
擴充套件方法註冊 TestServer
,可以使用 IHost
的 GetTestClient
擴充套件方法來註冊和 TestServer
進行互動的 HttpClient
服務註冊好之後就可以在測試用例裡通過注入的 HttpClient
請求 API 或頁面了,可以參考這個例子
Extensions
Hosted Service
然後我們來看一些擴充套件用法,IHostedService
可以用來實現一些初始化的操作或者後臺服務,我們可以使用 IHostedService
來實現對應用的 Ready 檢查,應用 Ready 之後再開始執行測試用例,這在有些場景下是很有用的
我們在 k8s 中部署的應用一般都會有一個 HealthCheck
/ReadinessCheck
的介面來供 k8s 的 liveness/readiness 探針來探測應用的狀態,只有應用 Ready 之後才會對外部提供服務
這個示例就是一個使用 IHostedService
來實現等待應用 Ready 後再開始執行測試用例的一個 demo
注意:這裡的等待不能在
Startup
的Configure
方法中執行,因為Configure
的執行是在呼叫 Host 的StartAsync
方法之前執行的,而此時 webServer 還沒有啟動,所以是不能獲取到TestClient
的,而我們通過HostedService
就可以在 Web Server 啟動之後再執行我們的等待 Ready 邏輯
ITestOutputHelperAccessor
在測試中如果想要輸出一個日誌的話只能藉助於 ITestOutputHelper
來輸出,直接使用 Console.Write[Line]
是看不到任何輸出的,ITestOutputHelper
只能在測試用例中使用,在測試服務中是不能使用的,Xunit.DependencyInjection
提供了一個 ITestOutputHelperAccessor
的服務,類似於 IHttpContextAccessor
,我們可以藉助它來在自定義的服務中獲取 ITestOutputHelper
來輸出日誌
這裡是一個簡單的示例
Logging
再來看一個 OutputHelperAccessor
的實際應用,Xunit.DependencyInjection
提供了一個 Logging 的擴充套件,使得我們可以把測試過程中的日誌輸出出來,更好的幫助我們除錯
整合方式也比較簡單,可以參考這個示例,引用 Xunit.DependencyInjection.Logging
之後,在 LoggerFactory
中註冊 XunitTestOutputLoggerProvider
即可
可以看到我們的日誌直接輸出出來了,預設的日誌級別是 Information
,所以 Debug
級別的日誌沒有輸出出來,有需要的話可以在註冊的時候提供一個委託來控制是否要輸出日誌
Project Template
為了方便大家使用,我們提供了一個專案模板,可以通過一個命令就可以直接建立好一個測試專案,會包含一個預設的 Startup
不再需要自己去寫方法了,使用的時候只需要根據需要做刪減就可以了
預設的 TargetFramework
使用的是 netcoreapp3.1
,可以通過 -f
/--franework
指定自己想要使用的目標框架,比如說想要生成 net 5.0 的專案只需要指定 -f net5.0
就可以了
生成的內容如下所示:
More
最後列出來了一些可能會有幫助的連結,第一個是專案的原始碼,第二個是 PPT 中所有示例的原始碼,後面的是使用到的 Nuget 包。
這個 xunit 擴充套件的程式碼實現是非常值得學習的,有很多和 asp.net core 的實現是很像的,有需要的可以去看看原始碼學習一下。
希望我的分享對大家有所幫助,大家在使用過程中有遇到任何問題都可以隨時聯絡我或者直接在 Github 上建 issue。
Reference
- https://github.com/pengweiqhca/Xunit.DependencyInjection
- https://github.com/WeihanLi/XunitDependencyInjection.Samples
- https://www.nuget.org/packages/Xunit.DependencyInjection
- https://www.nuget.org/packages/Microsoft.AspNetCore.TestHost
- https://www.nuget.org/packages/Xunit.DependencyInjection.Logging
- https://www.nuget.org/packages/Xunit.DependencyInjection.Template