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

如何对基于Entity Framework的仓储进行单元测试

问题:测试Entity Framework仓储层中的业务逻辑

我正在构建一个以Entity Framework作为仓储层的系统,我认同无需测试EF本身,但需要验证仓储中围绕EF的业务逻辑。在Stack Overflow及其他平台搜索后,多数回答仅建议不要测试框架,没找到可行的解决方案。

我的场景里,仓储需要将BillEntity类型转换为BillDal进行存储,账单的实体结构与存储结构存在明显差异:

  • BillEntity包含带Allocations的Line Items,且这些子对象没有ID
  • 存储对象将Line Items和Allocations以扁平化结构存储在账单中

这些映射、计算逻辑都属于仓储逻辑,必须进行测试。后续我还计划把当前简单的清除重加逻辑替换为更复杂的映射逻辑,同样需要测试覆盖。

我尝试过使用SQLite内存数据库,但由于模型中定义了排序规则(例如modelBuilder.UseCollation("utf8mb4_unicode_520_ci"),生产数据库为MariaDB),导致无法创建数据库。是否存在可直接替代、无需手动Mock全部内容的Entity Framework测试框架?

我知道可以单独测试辅助方法,但也希望能为仓储编写功能测试。以下是我想要单元测试的仓储代码、Dal类及实体类:

仓储代码

public class BillsMySqlRepository : BaseMySQLRepository, IBillsRepository
{
    public async Task<BillEntity> GetById(long id)
    {
        await using var context = await this.contextFactory.CreateDbContextAsync();
        var dal = await context.Bills
            .Include(x => x.LineItems)
            .Include(x => x.Allocations)
            .FirstOrDefaultAsync(x => x.Id == id);
        this.ThrowNotFoundIfNull(dal, "Bill");
        var entity = this.mapper.Map<BillEntity>(dal);
        entity.LineItems = this.CreateLineItemEntities(dal);
        return entity;
    }

    private List<BillLineItemEntity> CreateLineItemEntities(BillDal dal)
    {
        var lineItemEntities = new List<BillLineItemEntity>();
        var allocationMap = this.CreateAllocationMap(dal.Allocations);
        foreach (var lineItemDal in dal.LineItems)
        {
            var lineItemEntity = this.mapper.Map<BillLineItemEntity>(lineItemDal);
            lineItemEntity.Allocations = allocationMap.TryGetValue(lineItemDal.Id, out var value)
                ? value.Select(x => this.mapper.Map<BillAllocationEntity>(x)).ToList()
                : new List<BillAllocationEntity>();
            lineItemEntities.Add(lineItemEntity);
        }

        return lineItemEntities;
    }

    private Dictionary<long, List<BillAllocationDal>> CreateAllocationMap(List<BillAllocationDal> allocationDals)
    {
        var allocationMap = new Dictionary<long, List<BillAllocationDal>>();
        foreach (var allocation in allocationDals)
        {
            if (!allocationMap.ContainsKey(allocation.LineItemId))
            {
                allocationMap.Add(allocation.LineItemId, new List<BillAllocationDal>());
            }

            allocationMap[allocation.LineItemId].Add(allocation);
        }

        return allocationMap;
    }

    public async Task<long> Create(CreateOrUpdateBillEntity entity)
    {
        Guard.Against.InvalidCreateOrUpdateBillEntity(entity, nameof(entity));
        await using var context = await this.contextFactory.CreateDbContextAsync();
        var dal = this.mapper.Map<BillDal>(entity);
        this.AddId(dal);
        dal.CreatedMilliseconds = DateTimeOffset.Now.ToUnixTimeMilliseconds();
        dal.UpdatedMilliseconds = DateTimeOffset.Now.ToUnixTimeMilliseconds();
        this.AddLineItemAndAllocations(dal, entity);
        this.UpdateMetadataFields(dal, entity);
        context.Bills.Add(dal);
        await context.SaveChangesAsync();
        return dal.Id;
    }

    public async Task Update(long id, CreateOrUpdateBillEntity entity)
    {
        Guard.Against.NegativeOrZero(id, nameof(id));
        Guard.Against.InvalidCreateOrUpdateBillEntity(entity, nameof(entity));
        await using var context = await this.contextFactory.CreateDbContextAsync();
        var dal = await context.Bills
            .Include(x => x.LineItems)
            .Include(x => x.Allocations)
            .FirstOrDefaultAsync(x => x.Id == id);
        this.ThrowNotFoundIfNull(dal, "Bill");

        // TODO: Later we need to make this smarter that matches and updates instead of just clearing and re-adding
        context.BillLineItems.RemoveRange(dal.LineItems);
        context.BillAllocations.RemoveRange(dal.Allocations);
        this.AddLineItemAndAllocations(dal, entity);
        // TODO DONE

        this.UpdateMetadataFields(dal, entity);
        await context.SaveChangesAsync();
    }
}

Dal类

[Table("bills")]
[Index(nameof(CompanyId), Name = "index_bills_company_id")]
public class BillDal : IIdDal, ICompanyIdDal
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Column("id")]
    public long Id { get; set; }

    [Required]
    [Column("company_id")]
    public long CompanyId { get; set; }

    [StringLength(150)]
    [Column("reference")]
    public string? Reference { get; set; }

    [Column("memo")]
    public string? Memo { get; set; }

    [Column("source")]
    public long? Source { get; set; }

    [Column("source_type")]
    public BillSource? SourceType { get; set; }

    [Column("destination")]
    public long? Destination { get; set; }

    [Column("destination_type")]
    public BillSource? DestinationType { get; set; }

    [Column("property_id")]
    public long PropertyId { get; set; }

    [Column("bill_date_ms")]
    public long BillDateMilliseconds { get; set; }

    [Column("due_date_ms")]
    public long? DueDateMilliseconds { get; set; }

    [Required]
    [Column("create_ms")]
    public long CreatedMilliseconds { get; set; }

    [Required]
    [Column("updated_ms")]
    public long UpdatedMilliseconds { get; set; }

    // The following field are use as metadata to be able to fetch the bills quicker
    [Required]
    [Column("amount")]
    public decimal Amount { get; set; }

    [Required]
    [Column("amount")]
    public decimal Due { get; set; }


    [ForeignKey(nameof(CompanyId))]
    public virtual CompanyDal Company { get; set; }

    public virtual List<BillAllocationDal> Allocations { get; set; }
    public virtual List<BillLineItemDal> LineItems { get; set; }
}

[Table("bills_allocations")]
public class BillAllocationDal : IIdDal, ICompanyIdDal
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Column("id")]
    public long Id { get; set; }

    [Required]
    [Column("company_id")]
    public long CompanyId { get; set; }

    [Required]
    [Column("bill_id")]
    public long BillId { get; set; }

    [Required]
    [Column("bill_line_item_id")]
    public long LineItemId { get; set; }

    [Required]
    [Column("transaction_id")]
    public long TransactionId { get; set; }

    [Required]
    [Column("amount")]
    public decimal Amount { get; set; }

    [ForeignKey(nameof(BillId))]
    public virtual BillDal Bill { get; set; }

    [ForeignKey(nameof(CompanyId))]
    public virtual CompanyDal Company { get; set; }

    [ForeignKey(nameof(TransactionId))]
    public virtual TransactionDal Transaction { get; set; }

    [ForeignKey(nameof(LineItemId))]
    public virtual BillLineItemDal LineItem { get; set; }
}
[Table("bills_line_items")]
public class BillLineItemDal : IIdDal, ICompanyIdDal
{
    [Required]
    [Column("bill_id")]
    public long BillId { get; set; }

    [Required]
    [Column("chart_of_account_id")]
    public long ChartOfAccountId { get; set; }

    [StringLength(1024)]
    [Column("description")]
    public string Description { get; set; }

    [Required]
    [Column("amount")]
    public decimal Amount { get; set; }

    [ForeignKey(nameof(BillId))]
    public virtual BillDal Bill { get; set; }

    [ForeignKey(nameof(CompanyId))]
    public virtual CompanyDal Company { get; set; }

    [Required]
    [Column("company_id")]
    public long CompanyId { get; set; }

    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Column("id")]
    public long Id { get; set; }
}

实体类

public class BillEntity
{
    public long Id { get; set; }
    public long CompanyId { get; set; }
    public string? Reference { get; set; }
    public string? Memo { get; set; }
    public long VendorId { get; set; }
    public long PropertyId { get; set; }
    public DateTimeOffset BillDate { get; set; }
    public DateTimeOffset? DueDate { get; set; }

    public List<BillLineItemEntity> LineItems { get; set; }
}

public class BillLineItemEntity
{
    public long ChartOfAccountId { get; set; }
    public string? Description { get; set; }
    public decimal Amount { get; set; }
    public List<BillAllocationEntity> Allocations { get; set; }
}
public class BillAllocationEntity
{
    public long TransactionId { get; set; }
    public decimal Amount { get; set; }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 05:47:05