引言 上一篇我們創建了一個Sample.Api項目和Sample.Repository,並且帶大家熟悉了一下Moq的概念,這一章我們來實戰一下在xUnit項目使用依賴註入。 Xunit.DependencyInjection Xunit.DependencyInjection 是一個用於 xUnit ...
引言
上一篇我們創建了一個Sample.Api
項目和Sample.Repository
,並且帶大家熟悉了一下Moq
的概念,這一章我們來實戰一下在xUnit
項目使用依賴註入。
Xunit.DependencyInjection
Xunit.DependencyInjection
是一個用於 xUnit
測試框架的擴展庫,它提供了依賴註入的功能,使得在編寫單元測試時可以更方便地進行依賴註入。通過使用 Xunit.DependencyInjection
,可以在 xUnit
測試中使用依賴註入容器(比如 Microsoft.Extensions.DependencyInjection
)來管理測試中所需的各種依賴關係,包括服務、日誌、配置等等。
使用
我們用Xunit.DependencyInjection
對上一章的Sample.Repository
進行單元測試。
Nuget
包安裝項目依賴
PM> NuGet\Install-Package Xunit.DependencyInjection -Version 9.1.0
創建測試類
public class StaffRepositoryTest
{
[Fact]
public void DependencyInject_WhenCalled_ReturnTrue()
{
Assert.True(true);
}
}
運行測試 先看一下
從這可以得出一個結論 如果安裝了
Xunit.DependencyInjection
的xUnit
單元測試項目啟動時會檢測是否有預設的Startup
類
如果你安裝了Xunit.DependencyInjection
但是還沒有準備好在項目中使用也可以在csproj
中禁用
<Project>
<PropertyGroup>
<EnableXunitDependencyInjectionDefaultTestFrameworkAttribute>false</EnableXunitDependencyInjectionDefaultTestFrameworkAttribute>
</PropertyGroup>
</Project>
再測試一下
可以看到我們添加的配置生效了
配置
在我們的測試項目中新建Startup.cs
public class Startup
{
}
在.Net 6
之前我們不就是用這個來配置項目的依賴和管道嗎,其實這個位置也一樣用它來對我們項目的依賴和服務做一些基礎配置,使用配置單元測試的Startup
其實和配置我們的Asp.Net Core
的啟動配置是一樣的
CreateHostBuilder
CreateHostBuilder
方法用於創建應用程式的主機構建器(HostBuilder
)。在這個方法中,您可以配置主機的各種參數、服務、日誌、環境等。這個方法通常用於配置主機構建器的各種屬性,以便在應用程式啟動時使用。
public IHostBuilder CreateHostBuilder([AssemblyName assemblyName]) { }
ConfigureHost
ConfigureHost
方法用於配置主機構建器。在這個方法中,您可以對主機進行一些自定義的配置,比如設置環境、使用特定的配置源等
public void ConfigureHost(IHostBuilder hostBuilder) { }
ConfigureServices
ConfigureServices
方法用於配置依賴註入容器(ServiceCollection
)。在這個方法中,您可以註冊應用程式所需的各種服務、中間件、日誌、資料庫上下文等等。這個方法通常用於配置應用程式的依賴註入服務。
Configure
ConfigureServices
中配置的服務可以在 Configure
方法中指定。如果已經配置的服務在 Configure 方法的參數中可用,它們將會被註入
public void Configure()
{
}
Sample.Repository
接下來對我們的倉儲層進行單元測試
已知我們的倉儲層已經有註入的擴展方法
public static IServiceCollection AddEFCoreInMemoryAndRepository(this IServiceCollection services)
{
services.AddScoped<IStaffRepository, StaffRepository>();
services.AddDbContext<SampleDbContext>(options => options.UseInMemoryDatabase("sample").EnableSensitiveDataLogging(), ServiceLifetime.Scoped);
return services;
}
所以我們只需要在單元測試項目的Startup
的ConfigureServices
註入即可。
對我們的Sample.Repository
添加項目引用,然後進行依賴註冊
public void ConfigureServices(IServiceCollection services, HostBuilderContext context)
{
services.AddEFCoreInMemoryAndRepository();
}
好了接下來編寫單元測試Case
依賴項獲取:
public class StaffRepositoryTest
{
private readonly IStaffRepository _staffRepository;
public StaffRepositoryTest(IStaffRepository staffRepository)
{
_staffRepository = staffRepository;
}
}
在測試類中使用依賴註入和我們正常獲取依賴是一樣的都是通過構造函數的形式
public class StaffRepositoryTest
{
private readonly IStaffRepository _staffRepository;
public StaffRepositoryTest(IStaffRepository staffRepository)
{
_staffRepository = staffRepository;
}
//[Fact]
//public void DependencyInject_WhenCalled_ReturnTrue()
//{
// Assert.True(true);
//}
[Fact]
public async Task AddStaffAsync_WhenCalled_ShouldAddStaffToDatabase()
{
// Arrange
var staff = new Staff { Name = "zhangsan", Email = "[email protected]" };
// Act
await _staffRepository.AddStaffAsync(staff, CancellationToken.None);
// Assert
var retrievedStaff = await _staffRepository.GetStaffByIdAsync(staff.Id, CancellationToken.None);
Assert.NotNull(retrievedStaff); // 確保 Staff 已成功添加到資料庫
Assert.Equal("zhangsan", retrievedStaff.Name); // 檢查名稱是否正確
}
[Fact]
public async Task DeleteStaffAsync_WhenCalled_ShouldDeleteStaffFromDatabase()
{
var staff = new Staff { Name = "John", Email = "[email protected]" };
await _staffRepository.AddStaffAsync(staff, CancellationToken.None); // 先添加一個 Staff
// Act
await _staffRepository.DeleteStaffAsync(staff.Id, CancellationToken.None); // 刪除該 Staff
// Assert
var retrievedStaff = await _staffRepository.GetStaffByIdAsync(staff.Id, CancellationToken.None); // 嘗試獲取已刪除的 Staff
Assert.Null(retrievedStaff); // 確保已經刪除
}
[Fact]
public async Task UpdateStaffAsync_WhenCalled_ShouldUpdateStaffInDatabase()
{
// Arrange
var staff = new Staff { Name = "John", Email = "[email protected]" };
await _staffRepository.AddStaffAsync(staff, CancellationToken.None); // 先添加一個 Staff
// Act
staff.Name = "Updated Name";
await _staffRepository.UpdateStaffAsync(staff, CancellationToken.None); // 更新 Staff
// Assert
var updatedStaff = await _staffRepository.GetStaffByIdAsync(staff.Id, CancellationToken.None); // 獲取已更新的 Staff
Assert.Equal("Updated Name", updatedStaff?.Name); // 確保 Staff 已更新
}
[Fact]
public async Task GetStaffByIdAsync_WhenCalledWithValidId_ShouldReturnStaffFromDatabase()
{
// Arrange
var staff = new Staff { Name = "John", Email = "[email protected]" };
await _staffRepository.AddStaffAsync(staff, CancellationToken.None); // 先添加一個 Staff
// Act
var retrievedStaff = await _staffRepository.GetStaffByIdAsync(staff.Id, CancellationToken.None); // 獲取 Staff
// Assert
Assert.NotNull(retrievedStaff); // 確保成功獲取 Staff
}
[Fact]
public async Task GetAllStaffAsync_WhenCalled_ShouldReturnAllStaffFromDatabase()
{
// Arrange
var staff1 = new Staff { Name = "John", Email = "[email protected]" };
var staff2 = new Staff { Name = "Alice", Email = "[email protected]" };
await _staffRepository.AddStaffAsync(staff1, CancellationToken.None); // 先添加 Staff1
await _staffRepository.AddStaffAsync(staff2, CancellationToken.None); // 再添加 Staff2
// Act
var allStaff = await _staffRepository.GetAllStaffAsync(CancellationToken.None); // 獲取所有 Staff
// Assert
List<Staff> addStaffs = [staff1, staff2];
Assert.True(addStaffs.All(_ => allStaff.Any(x => x.Id == _.Id))); // 確保成功獲取所有 Staff
}
}
Run Tests
可以看到單元測試已經都成功了,是不是很簡單呢。
擴展
如何註入 ITestOutputHelper?
之前的示例不使用xUnit.DependencyInjection
我們用ITestOutputHelper
通過構造函數構造,現在是用ITestOutputHelperAccessor
public class DependencyInjectionTest
{
private readonly ITestOutputHelperAccessor _testOutputHelperAccessor;
public DependencyInjectionTest(ITestOutputHelperAccessor testOutputHelperAccessor)
{
_testOutputHelperAccessor = testOutputHelperAccessor;
}
[Fact]
public void TestOutPut_Console()
{
_testOutputHelperAccessor.Output?.WriteLine("測試ITestOutputHelperAccessor");
Assert.True(true);
}
}
OutPut:
日誌輸出到 ITestOutputHelper
Nuget
安裝
PM> NuGet\Install-Package Xunit.DependencyInjection.Logging -Version 9.0.0
ConfigureServices
配置依賴
public void ConfigureServices(IServiceCollection services)
=> services.AddLogging(lb => lb.AddXunitOutput());
使用:
public class DependencyInjectionTest
{
private readonly ILogger<DependencyInjectionTest> _logger;
public DependencyInjectionTest(ILogger<DependencyInjectionTest> logger)
{
_logger = logger;
}
[Fact]
public void Test()
{
_logger.LogDebug("LogDebug");
_logger.LogInformation("LogInformation");
_logger.LogError("LogError");
}
}
OutPut:
標準輸出:
[2024-04-12 16:00:24Z] info: dotNetParadise.DependencyInjection.DependencyInjectionTest[0]
LogInformation
[2024-04-12 16:00:24Z] fail: dotNetParadise.DependencyInjection.DependencyInjectionTest[0]
LogError
startup 類中註入 IConfiguration 或 IHostEnvironment
通過ConfigureServices
設置 EnvironmentName
和使用IConfiguration
public void ConfigureServices(HostBuilderContext context)
{
context.HostingEnvironment.EnvironmentName = "test";
//使用配置
context.Configuration.GetChildren();
}
也可以使用Startup
下的ConfigureHost
設置
public class Startup
{
public void ConfigureHost(IHostBuilder hostBuilder) =>
hostBuilder
.ConfigureServices((context, services) => { context.XXXX });
}
在 ConfigureHost 下可以對.Net
IHostBuilder
進行配置,可以對IConfiguration
,IServiceCollection
,Log
等跟Asp.Net Core
使用一致。
集成測試
xUnit.DependencyInjection
也可以對Asp.Net Core
項目進行集成測試
安裝 Microsoft.AspNetCore.TestHost
PM> NuGet\Install-Package Microsoft.AspNetCore.TestHost -Version 9.0.0-preview.3.24172.13
public void ConfigureHost(IHostBuilder hostBuilder) =>
hostBuilder.ConfigureWebHost[Defaults](webHostBuilder => webHostBuilder
.UseTestServer(options => options.PreserveExecutionContext = true)
.UseStartup<AspNetCoreStartup>());
可以參考 xUnit 的官網實現,其實有更優雅的實現集成測試的方案,xUnit.DependencyInject
的集成測試方案僅做參考集合,在後面章節筆者會對集成測試做詳細的介紹。
最後
希望本文對您在使用 Xunit.DependencyInjection
進行依賴註入和編寫單元測試時有所幫助。通過本文的介紹,您可以更加靈活地管理測試項目中的依賴關係,提高測試代碼的可維護性和可測試性