开始在 ASP.NET Core 中进行单元测试

发布:2024-10-15 11:00 阅读: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 创建了 IDeviceRepositoryIUserContextService 的模拟对象。
  • Arrange 步骤中设置了虚拟设备数据,Act 步骤执行测试方法,Assert 步骤验证结果是否符合预期。

七. 结论

单元测试是确保ASP.NET Core应用程序代码质量的关键。它不仅能帮助尽早发现Bug,还能保证代码的可维护性,促进CI/CD管道的自动化集成和部署,从而提高开发效率并降低开发成本。