簡介
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 官方的默認模版也是這樣,集成測驗基類的核心代碼很簡單,就是在無參建構式的內部進行初始化動作,且在程序中按順序執行兩個生命周期方法,
簡易流程圖:

public abstract class AbpIntegratedTest<TStartupModule> : AbpTestBaseWithServiceProvider, IDisposable
where TStartupModule : IAbpModule
{
protected IAbpApplication Application { get; }
protected override IServiceProvider ServiceProvider => Application.ServiceProvider;
protected IServiceProvider RootServiceProvider { get; }
protected IServiceScope TestServiceScope { get; }
protected AbpIntegratedTest()
{
var services = CreateServiceCollection();
BeforeAddApplication(services);
var application = services.AddApplication<TStartupModule>(SetAbpApplicationCreationOptions);
Application = application;
AfterAddApplication(services);
// 根據已有 IServiceCollection 創建 IoC 容器,
RootServiceProvider = CreateServiceProvider(services);
TestServiceScope = RootServiceProvider.CreateScope();
// 使用子容器對 ABP 模塊系統進行初始化,
application.Initialize(TestServiceScope.ServiceProvider);
}
// ... 其他代碼,
}
上述代碼可以看到默認的測驗基類并沒有直接使用 RootServiceProvider,而是創建了一個子容器給 ABP vNext 使用,這主要是為了后續可以對容器進行銷毀操作,具體可一看下面的 Dispose() 方法,
public virtual void Dispose()
{
Application.Shutdown();
TestServiceScope.Dispose();
Application.Dispose();
}
總的來說,測驗基類就是構建了一個 IAbpApplication 物件,根據傳入的 TStartupModule 模塊進入拓撲排序程序,依次執行各個模塊的生命周期配置,
MVC 測驗基類
針對我們的 Http Api 層,如果需要對 Controller 進行測驗的話,就需要從 MVC 測驗基類繼承撰寫單元/集成測驗,各個型別的關系如下,
針對 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,但可以保證每次進行業務修改之后介面的正確性,
- 點擊我跳轉到總目錄
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/280927.html
標籤:.NET技术
