- A+
引言
上一篇我们创建了一个Sample.Api
项目和Sample.Repository
,并且带大家熟悉了一下Moq
的概念,这一章我们来实战一下在xUnit
项目使用依赖注入。
Xunit.DependencyInjection
Xunit.DependencyInjection
是一个用于 xUnit
测试框架的扩展库,它提供了依赖注入的功能,使得在编写单元测试时可以更方便地进行依赖注入。通过使用 Xunit.DependencyInjection
,可以在 xUnit
测试中使用依赖注入容器(比如 Microsoft.Extensions.DependencyInjection
)来管理测试中所需的各种依赖关系,包括服务、日志、配置等等。
使用
我们用Xunit.DependencyInjection
对上一章的Sample.Repository
进行单元测试。
Nuget
包安装项目依赖
PM> NuGetInstall-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 = "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
可以看到单元测试已经都成功了,是不是很简单呢。
扩展
如何注入 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> NuGetInstall-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> NuGetInstall-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
进行依赖注入和编写单元测试时有所帮助。通过本文的介绍,您可以更加灵活地管理测试项目中的依赖关系,提高测试代码的可维护性和可测试性
?欢迎关注笔者公众号一起学习交流,获取更多有用的知识~