开始在 ASP.NET Core 中进行单元测试
阅读:16
点赞:0
一. 引言
单元测试是一种测试最小功能单元的过程。通常通过为代码的特定单元提供虚拟输入,测试其逻辑是否正确。单元测试通常包含三个步骤:**安排(Arrange)、执行(Act)和断言(Assert)**。
二. 单元测试在现代Web开发中的重要性
2.1 提高代码质量
通过为代码编写单元测试,可以验证每一行代码是否按预期工作。
2.2 促进可维护性
良好的测试帮助更快定位问题根源,提升代码的可维护性。
2.3 提高Bug检测能力
通过为代码提供虚拟数据,可以更容易检测出可能的Bug。
2.4 促进持续集成与交付
将单元测试集成到CI/CD流程中,可以确保代码在部署前先通过所有测试。
接下来我们通过代码实践,深入理解这些概念。
三. 设置测试环境
ASP.NET Core 提供了多种测试框架,比如:
-
xUnit -
NUnit -
MSTest
本示例我们将使用xUnit框架。可以通过 Visual Studio 创建一个xUnit测试项目,也可以通过 dotnet CLI
来创建项目:
dotnet new sln -o unit-testing-using-dotnet-test
四. 安装所需的NuGet包
我们需要安装以下 NuGet 包来进行单元测试:
-
AutoFixture:用于生成虚拟数据。 dotnet add package AutoFixture
-
Moq:用于生成依赖项的模拟对象。 dotnet add package Moq
-
FluentAssertions:用于更简洁地进行测试断言。 dotnet add package FluentAssertions
五. 编写单元测试
我们将针对一个位于应用程序层的 CompanyService
服务编写单元测试。应用程序层负责处理HTTP请求、执行操作,并将结果返回给API层。以下是 CompanyService
的代码:
public sealed class CompanyService
{
private readonly ApplicationDbContext _context;
// 通过构造函数注入数据库上下文
public CompanyService(ApplicationDbContext context)
{
_context = context;
}
// 通过公司ID异步获取公司对象
public async Task<Company> GetCompanyAsync(Guid id)
{
var company = await _context.Companies.FindAsync(id);
return company!;
}
// 通过公司名称异步获取公司对象
public async Task<Company> GetCompanyAsync(string companyName)
{
var company = await _context.Companies.SingleOrDefaultAsync(e => e.Name.Equals(companyName));
return company!;
}
// 创建公司对象并保存到数据库,返回公司ID
public async Task<Guid> CreateCompany(string name)
{
var company = Company.Create(name);
_context.Companies.Add(company);
await _context.SaveChangesAsync();
return company.Id;
}
}
接下来我们将编写单元测试类 CompanyServiceTests
,并对每个服务方法进行测试。
public class CompanyServiceTests : IDisposable
{
private readonly ApplicationDbContext _dbContext;
private readonly CompanyService _companyService;
// 测试类构造函数,设置数据库上下文和服务实例
public CompanyServiceTests()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: "TestDatabase")
.Options;
_dbContext = new ApplicationDbContext(options);
_companyService = new CompanyService(_dbContext);
}
// 测试:当公司存在时,通过ID获取公司对象
[Fact]
public async Task GetCompanyById_ShouldReturnCompany_WhenCompanyExists()
{
// Arrange: 设置测试数据
var companyId = Guid.NewGuid();
var company = new Company { Id = companyId, Name = "Test Company" };
_dbContext.Companies.Add(company);
await _dbContext.SaveChangesAsync();
// Act: 执行获取公司操作
var result = await _companyService.GetCompanyAsync(companyId);
// Assert: 断言结果是否符合预期
Assert.NotNull(result);
Assert.Equal(companyId, result.Id);
Assert.Equal("Test Company", result.Name);
}
// 测试:当公司不存在时,成功创建公司
[Fact]
public async Task ShouldCreateCompany_WhenCompanyNotExists()
{
// Arrange: 准备公司数据
var company = Company.Create("Test Company");
// Act: 执行创建公司操作
var result = await _companyService.CreateCompany(company.Name);
// Assert: 断言公司ID是否有效
result.Should().NotBeEmpty();
}
// 测试:当公司存在时,通过公司名称获取公司对象
[Fact]
public async Task GetCompanyByName_ShouldReturnCompany_WhenCompanyExists()
{
// Arrange: 准备测试数据
var companyId = Guid.NewGuid();
var company = new Company { Id = companyId, Name = "Test 1 Company" };
_dbContext.Companies.Add(company);
await _dbContext.SaveChangesAsync();
// Act: 执行获取公司操作
var result = await _companyService.GetCompanyAsync(company.Name);
// Assert: 断言结果是否符合预期
Assert.NotNull(result);
Assert.Equal(companyId, result.Id);
Assert.Equal("Test 1 Company", result.Name);
}
// 释放数据库上下文资源
public void Dispose()
{
_dbContext.Dispose();
}
}
代码详解
-
我们使用 InMemoryDatabase
设置数据库上下文,用于在测试中模拟真实的数据库操作。 -
Arrange
步骤中我们设置了所需的输入,Act
步骤执行实际的测试方法,最后在Assert
步骤中验证输出是否正确。 -
IDisposable
接口用于在测试结束后释放数据库资源。
六. 处理仓储接口的单元测试
在 DeviceService
中,我们通过依赖注入的方式使用了 IDeviceRepository
处理数据库操作。接下来展示如何为其编写单元测试:
public class DeviceService
{
private readonly IDeviceRepository _deviceRepository;
private readonly IUserContextService _userContextService;
// 构造函数注入设备仓储和用户上下文服务
public DeviceService(IDeviceRepository deviceRepository, IUserContextService userContextService)
{
_deviceRepository = deviceRepository;
_userContextService = userContextService;
}
// 异步获取设备信息并转换为设备传输对象DTO
public async Task<GetDevicesListDto> GetDeviceAsync(Guid id)
{
var device = await _deviceRepository.Get(id);
if (device == null)
{
throw new InvalidOperationException("Device not found.");
}
var deviceDto = new GetDevicesListDto
{
Id = id,
Name = device.Name,
Date = device.CreatedOn,
TypeId = device.Type,
Location = device.Location
};
return deviceDto;
}
}
接下来是 DeviceServiceTests
单元测试类:
public class DeviceServiceTests
{
private readonly IFixture _fixture;
private readonly Mock<IDeviceRepository> _deviceRepository;
private readonly Mock<IUserContextService> _userContextService;
private readonly DeviceService _deviceService;
// 测试类构造函数,设置依赖项的模拟对象和服务实例
public DeviceServiceTests()
{
_fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
_deviceRepository = new Mock<IDeviceRepository>();
_userContextService = new Mock<IUserContextService>();
_deviceService = new DeviceService(_deviceRepository.Object, _userContextService.Object);
}
// 测试:通过设备ID获取设备信息
[Fact]
public async Task Should_Get_Device_Async()
{
// Arrange: 设置虚拟数据
var id = Guid.NewGuid();
var device = _fixture.Create<Device>();
_deviceRepository.Setup(x => x.Get(It.IsAny<Guid>())).Returns(Task.FromResult(device));
// Act: 执行获取设备操作
var result = await _deviceService.GetDeviceAsync(id);
// Assert: 断言结果是否符合预期
result.Should().BeAssignableTo<GetDevicesListDto>();
Assert.NotNull(result);
}
}
代码详解
-
我们使用 Moq
创建了IDeviceRepository
和IUserContextService
的模拟对象。 -
在 Arrange
步骤中设置了虚拟设备数据,Act
步骤执行测试方法,Assert
步骤验证结果是否符合预期。
七. 结论
单元测试是确保ASP.NET Core应用程序代码质量的关键。它不仅能帮助尽早发现Bug,还能保证代码的可维护性,促进CI/CD管道的自动化集成和部署,从而提高开发效率并降低开发成本。