• 实战指南:使用 xUnit.DependencyInjection 在单元测试中实现依赖注入【完整教程】


    引言

    上一篇我们创建了一个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>falseEnableXunitDependencyInjectionDefaultTestFrameworkAttribute>
        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();
            services.AddDbContext(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 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 _logger;
        public DependencyInjectionTest(ILogger 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());
    

    可以参考 xUnit 的官网实现,其实有更优雅的实现集成测试的方案,xUnit.DependencyInject 的集成测试方案仅做参考集合,在后面章节笔者会对集成测试做详细的介绍。

    最后

    希望本文对您在使用 Xunit.DependencyInjection 进行依赖注入和编写单元测试时有所帮助。通过本文的介绍,您可以更加灵活地管理测试项目中的依赖关系,提高测试代码的可维护性和可测试性

    😄欢迎关注笔者公众号一起学习交流,获取更多有用的知识~
    image


    __EOF__

  • 本文作者: 董瑞鹏
  • 本文链接: https://www.cnblogs.com/ruipeng/p/18134907
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    JVM虚拟机知识点(保姆级教程)
    恒驰Q&A | 我们到底是做什么的?和恒大恒驰有什么关系?
    多层感知机学习XOR实例
    java计算机毕业设计小区广告位招商系统源码+系统+数据库+lw文档+mybatis+运行部署
    Dubbo 一些你不一定知道但是很好用的功能
    怎么把本机设置成代理ip?
    你真正了解什么是接口测试么?接口实战一“篇”入魂
    深圳市商务局2022年度中央资金(跨境电子商务企业市场开拓扶持事项)申报指南
    两分钟快速安装 ShardingSphere-Proxy(5.2.1)
    编程思想重新理解
  • 原文地址:https://www.cnblogs.com/ruipeng/p/18134907