[Abp vNext 源码分析] - 18. 单元测试
簡介
ABP vNext 框架使用?xUnit?作為單元測試組件,官方的所有模塊都編寫了大量的?單元/集成測試?確保功能正常。由于 ABP vNext 模塊化系統的原因,開發人員在建立單元測試項目的時候需要集成?Volo.Abp.UnitTest?項目,這樣在執行單元測試的時候才不會缺少必要組件。
分析
ABP vNext 單元測試相關的類型最核心的是集成測試基類?AbpIntegratedTest?和 MVC 專用測試基類?AbpAspNetCoreIntegratedTestBase,這兩個基類核心工作就是初始化 IoC 容器并且初始化整個模塊系統,只不過后者對?控制器?相關的組件進行了初始化配置,讓開發人員可以針對?控制器?進行單元/集成測試。
從上圖可以看到兩個基類都繼承自?AbpTestBaseWithServiceProvider?基類,在這個基類里面將?IServiceProvider?作為一個抽象成員。這是因為 MVC 和測試基類的?ServiceProvider?來源不一樣,一個是 ABP vNext 根據?Application?類已注冊 IoC 容器構建的,另一個使用的是?IHost?測試主機內的?ServiceProvider。
單元測試執行本質上就是將測試類進行實例化,然后調用對應的單元測試方法,所以測試基類的初始化動作都是放在對應的無參構造函數。
雖然?Volo.Abp.UnitTest?也是單獨的一個項目,它的?AbpTestBaseModule?是沒有任何動作,僅僅是為了同其他項目保持一致,內部是沒有任何代碼。
using Volo.Abp.Modularity;namespace Volo.Abp {public class AbpTestBaseModule : AbpModule{} }集成測試基類
一般來說,我們會直接從?AbpIntegratedTest?繼承并實現我們需要的單元測試基類,包括 ABP vNext 官方的默認模版也是這樣。集成測試基類的核心代碼很簡單,就是在無參構造函數的內部進行初始化動作,且在過程中按順序執行兩個生命周期方法。
簡易流程圖:
start=>start: 開始單元測試 op1=>operation: BeforeAddApplication() op2=>operation: 注冊模塊服務 op3=>operation: AfterAddApplication() op4=>operation: 模塊初始化 op5=>operation: 執行單元測試 end=>end: 銷毀相關資源
start->op1->op2->op3->op4->op5->end
上述代碼可以看到默認的測試基類并沒有直接使用?RootServiceProvider,而是創建了一個子容器給 ABP vNext 使用,這主要是為了后續可以對容器進行銷毀操作,具體可一看下面的?Dispose()?方法。
public virtual void Dispose() {Application.Shutdown();TestServiceScope.Dispose();Application.Dispose(); }總的來說,測試基類就是構建了一個?IAbpApplication?對象,根據傳入的?TStartupModule?模塊進入拓撲排序過程,依次執行各個模塊的生命周期配置。
MVC 測試基類
針對我們的 Http Api 層,如果需要對?Controller?進行測試的話,就需要從 MVC 測試基類繼承編寫單元/集成測試,各個類型的關系如下。
AbpTestBaseWithServiceProvider#ServiceProvider#GetService() : <T>#GetRequiredService() : <T>AbpIntegratedTest<TStartupModule>#BeforeAddApplication(IServiceCollection service)#SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)#AfterAddApplication(IServiceCollection services)#CreateServiceProvider(IServiceCollection service)+Dispose()AbpAspNetCoreIntegratedTestBase<TStartup>#TestServer Server#HttpClient Client-IHost host#CreateHostBuilder() : IHostBuilder#ConfigureServices(HostBuilderContext context, IServiceCollection services)#GetUrl_OfType_TController() : string#GetUrl_OfType_TController(string actionName) : string#GetUrl_OfType_TController(string actionName, object queryStringParamsAsAnonymousObject) : string+Dispose()IDispose<<interface>> IDispose
針對 AspNetCore 來說,ABP 創建了一個新的 Host 主機,在每次執行測試的時候會啟動一個新的 Web 服務器。(并不會創建真實服務,不存在端口占用問題)
在基類當中,ABP 定義了兩個屬性?Server?和?Client,它們都是 Mock 了對應的接口,方便后續的單元測試,這里的?ITestServerAccessor?接口是用于 Mock?AspNetCoreTestDynamicProxyHttpClientFactory?接口所需要的。
AspNetCoreTestDynamicProxyHttpClientFactory?接口是 ABP 底層進行動態代理所使用的,在請求遠程服務的時候會調用這個接口創建 HttpClient 對象。
protected AbpAspNetCoreIntegratedTestBase() {var builder = CreateHostBuilder();_host = builder.Build();_host.Start();Server = _host.GetTestServer();Client = _host.GetTestClient();ServiceProvider = Server.Services;ServiceProvider.GetRequiredService<ITestServerAccessor>().Server = Server; }從 UML 類圖當中,可以看到基類定義了幾個?GetUrl()?方法,這幾個方法是根據 Controller 獲取對應的請求路徑。
這里我以一個?SampleController?控制器為例,它提供了一個 Index 方法,返回了一個頁面內容。針對它來說,我們編寫集成測試是這樣操作的。
public class SimpleController : AbpController {public ActionResult Index(){return Content("Index-Result");} }集成測試
public class SimpleController_Tests : AbpAspNetCoreIntegratedTestBase<Startup> {protected virtual async Task<HttpResponseMessage> GetResponseAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK){using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url)){requestMessage.Headers.Add("Accept-Language", CultureInfo.CurrentUICulture.Name);var response = await Client.SendAsync(requestMessage);response.StatusCode.ShouldBe(expectedStatusCode);return response;}}protected virtual async Task<string> GetResponseAsStringAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK){using (var response = await GetResponseAsync(url, expectedStatusCode)){return await response.Content.ReadAsStringAsync();}}[Fact]public async Task ActionResult_ContentResult(){var result = await GetResponseAsStringAsync(GetUrl<SimpleController>(nameof(SimpleController.Index)));result.ShouldBe("Index-Result");} }EF Core 的集成
在執行單元測試過程中,我們難免會對數據庫進行操作。這個時候不可能連接真實數據庫,就需要我們在測試基類當中進行一些初始化動作,將底層的數據庫鏈接改為 SQLite 的內存模式。
public class SampleEntityFrameworkCoreTestModule : AbpModule {private SqliteConnection _sqliteConnection;public override void ConfigureServices(ServiceConfigurationContext context){ConfigureInMemorySqlite(context.Services);}private void ConfigureInMemorySqlite(IServiceCollection services){// 建立鏈接并執行遷移。_sqliteConnection = CreateDatabaseAndGetConnection();// 使用 SQLite 作為 EF Provider。services.Configure<AbpDbContextOptions>(options =>{options.Configure(context =>{context.DbContextOptions.UseSqlite(_sqliteConnection);});});}public override void OnApplicationShutdown(ApplicationShutdownContext context){_sqliteConnection.Dispose();}private static SqliteConnection CreateDatabaseAndGetConnection(){// 使用 SQLite 的內存模式鏈接字符串。var connection = new SqliteConnection("Data Source=:memory:");connection.Open();var options = new DbContextOptionsBuilder<SampleMigrationsDbContext>().UseSqlite(connection).Options;// 執行遷移,構建表結構。using (var context = new SampleMigrationsDbContext(options)){context.GetService<IRelationalDatabaseCreator>().CreateTables();}return connection;} }總結
ABP 的測試更偏向于集成測試,因為各個功能都依賴于模塊,所以在執行單元測試的時候會運行更長的時間。日常開發過程當中,我們更多地還是針對應用層進行測試就可以了,粒度更細的話也可以針對倉儲層、領域層、API 層?編寫測試即可。
為了保證項目質量,在開發完成之后編寫單元/集成測試是每個開發人員應做的工作。編寫單元/集成測試,雖然不能 100% 避免 BUG,但可以保證每次進行業務修改之后接口的正確性。
總結
以上是生活随笔為你收集整理的[Abp vNext 源码分析] - 18. 单元测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 记一次 .NET 某教育系统 异常崩溃分
- 下一篇: ASP.NET Core 单元测试:如何