You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Core API实体唯一属性的单元测试实现方案咨询

问题描述

实体定义

[Index(nameof(Name), IsUnique = true)]
public class AccountType:IEntityBase
{
    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public string Id { get; set; }
    [Required]
    [StringLength(100)]
    public string Name { get; set; }
}

待测试代码

public async Task<string> CreateEntity(string entityName)
{
    TEntity entity = new TEntity { Name = entityName };
    try
    {
        //将实体保存到数据库的代码
    }
    catch (DbUpdateException ex) when (ex.InnerException.HResult == -2147467259)
    {                        
        //处理异常的代码
    }
    return entity.Id;
}

需解决两个核心问题:

  1. 针对上述代码应当编写怎样的单元测试?
  2. 插入相同名称时,测试项目中未抛出DbUpdateException(但生产环境可正常触发),该如何修复?

解决方案

一、单元测试设计

针对CreateEntity方法,需覆盖两种核心业务场景:

1. 正常创建实体测试

  • 测试逻辑:传入唯一名称调用方法,验证返回非空ID,且数据库中存在对应名称的实体。
  • 示例代码(基于xUnit + EF Core测试数据库):
[Fact]
public async Task CreateEntity_WithUniqueName_ReturnsValidId()
{
    // 初始化测试数据库上下文
    await using var context = new YourDbContext(GetTestDbOptions());
    var service = new YourService(context);

    // 执行方法
    var entityId = await service.CreateEntity("TestAccountType");

    // 断言验证
    Assert.NotNull(entityId);
    var createdEntity = await context.AccountTypes.FindAsync(entityId);
    Assert.Equal("TestAccountType", createdEntity.Name);
}

2. 重复名称插入的异常处理测试

  • 测试逻辑:先插入一个实体,再调用方法传入同名参数,验证触发DbUpdateException且异常处理逻辑生效。
  • 示例代码:
[Fact]
public async Task CreateEntity_WithDuplicateName_TriggersExceptionHandling()
{
    await using var context = new YourDbContext(GetTestDbOptions());
    var service = new YourService(context);

    // 预先插入同名实体
    await service.CreateEntity("DuplicateType");
    await context.SaveChangesAsync();

    // 再次插入同名实体
    var entityId = await service.CreateEntity("DuplicateType");
    
    // 根据实际异常处理逻辑调整断言:比如返回已存在实体的ID
    var existingEntity = await context.AccountTypes.FirstAsync(t => t.Name == "DuplicateType");
    Assert.Equal(existingEntity.Id, entityId);
}

二、测试项目不抛异常的原因及修复方案

1. 内存数据库未启用约束校验

EF Core的InMemory数据库默认不强制执行唯一索引、外键等数据库层面约束,仅做模型字段校验,导致插入同名实体不会触发异常。

修复方案:改用SQLite内存数据库并开启约束校验:

private DbContextOptions<YourDbContext> GetTestDbOptions()
{
    var options = new DbContextOptionsBuilder<YourDbContext>()
        .UseSqlite("DataSource=:memory:", x => x.SuppressForeignKeyEnforcement(false))
        .Options;

    // SQLite内存数据库需手动打开连接才能保留数据
    using var context = new YourDbContext(options);
    context.Database.OpenConnection();
    context.Database.EnsureCreated();

    return options;
}

2. Mock的DbContext未模拟异常抛出

如果使用Moq等框架模拟DbContext,默认不会抛出数据库异常,需手动配置SaveChangesAsync抛出指定异常:

[Fact]
public async Task CreateEntity_WithDuplicateName_CatchesDbUpdateException()
{
    var mockContext = new Mock<YourDbContext>();
    // 模拟唯一约束冲突的异常
    var innerException = new SqlException("Unique constraint violation", -2147467259);
    mockContext.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()))
               .ThrowsAsync(new DbUpdateException("Update failed", innerException));

    var service = new YourService(mockContext.Object);
    await service.CreateEntity("DuplicateType");

    // 断言异常处理逻辑已执行(根据业务需求调整)
}

3. 异常过滤的HResult不匹配

不同数据库的唯一约束异常HResult/错误码不同(比如SQL Server与SQLite错误码不一致),测试用数据库与生产环境类型不同时,会导致when条件不触发。

修复方案:避免依赖硬编码的HResult,改用数据库通用的错误码判断:

catch (DbUpdateException ex) 
{
    // SQL Server唯一约束冲突错误码为2601,SQLite为19
    if (ex.InnerException is SqlException sqlEx && 
        (sqlEx.Number == 2601 || sqlEx.SqliteErrorCode == 19))
    {
        // 处理异常的代码
    }
}

内容的提问来源于stack exchange,提问作者Manta

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 04:11:03