實戰指南:使用 xUnit.DependencyInjection 在單元測試中實現依賴注入【完整教程】

董瑞鹏發表於2024-04-15

引言

上一篇我們建立了一個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);
    }
}

執行測試 先看一下

image

從這可以得出一個結論 如果安裝了Xunit.DependencyInjectionxUnit單元測試專案啟動時會檢測是否有預設的Startup

如果你安裝了Xunit.DependencyInjection但是還沒有準備好在專案中使用也可以在csproj中禁用

<Project>
    <PropertyGroup>
        <EnableXunitDependencyInjectionDefaultTestFrameworkAttribute>false</EnableXunitDependencyInjectionDefaultTestFrameworkAttribute>
    </PropertyGroup>
</Project>

再測試一下

image

可以看到我們新增的配置生效了

配置

在我們的測試專案中新建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;
    }

所以我們只需要在單元測試專案的StartupConfigureServices 注入即可。
對我們的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 = "zhangsan@163.com" };
        // 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 = "john@example.com" };
        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 = "john@example.com" };
        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 = "john@example.com" };
        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 = "john@example.com" };
        var staff2 = new Staff { Name = "Alice", Email = "alice@example.com" };
        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

image

可以看到單元測試已經都成功了,是不是很簡單呢。

擴充套件

如何注入 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:

image

日誌輸出到 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 進行依賴注入和編寫單元測試時有所幫助。透過本文的介紹,您可以更加靈活地管理測試專案中的依賴關係,提高測試程式碼的可維護性和可測試性

😄歡迎關注筆者公眾號一起學習交流,獲取更多有用的知識~
image

  • Xunit.DependencyInjection Github
  • 本文完整原始碼

相關文章