如何使用实体框架对存储库进行单元测试

问题描述 投票:0回答:1

我正在使用实体框架作为我的存储库构建一个系统,虽然我同意我不应该测试实体框架,但我希望能够验证我的存储库中围绕实体框架的逻辑。然而,我在 Stack Overflow 上或其他地方都无法找到真正的解决方案。大多数问题最终都会导致人们说不要测试框架。

在我的示例中,我有一个存储库,它采用 BillEntity 类型并将其存储为 BillDal,现在我的账单的存储计划与我的实体不同。示例 账单实体具有行项目,而行项目又具有分配,但它们没有 ID。但是,我的存储对象将行项目和分配存储为账单上的平面对象。这就是所有存储库逻辑,并且应该测试其映射和计算。后来我想用更复杂的映射逻辑替换我的 dum、clear 和 readd 逻辑,这些逻辑也需要测试。

我尝试在内存数据库中使用 SQLLite,但无法创建它,因为我在模型上定义了“排序规则”,例如

modelBuilder.UseCollation("utf8mb4_unicode_520_ci")
我的数据库是 MariaDb。是否存在可以插入并模拟 EntityFramework 的测试框架,而无需我自己模拟所有内容? 我知道我可以只测试我的辅助方法,但我也希望能够为我的存储库编写功能测试。

这是我想要进行单元测试的存储库代码:

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();
    }
}

达尔斯:

[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; }
}
c# entity-framework unit-testing entity-framework-core functional-testing
1个回答
0
投票

存储库模式超越 EF DbContext/DbSets 的一个理由是促进测试边界,这意味着存储库是您模拟的内容,而不是您测试的内容。您不需要编写测试来确认 EF 执行其应有的操作,或者 Automapper 执行其预期的操作。这些产品已经过测试,值得信赖。存储库不应包含任何需要测试的业务逻辑。最坏的情况是,如果存在一些边界逻辑,例如处理添加/删除场景而不是转储引用列表和重新添加引用,则该逻辑可以分为可测试的类。 IE。提供两个 ID 列表进行比较的方法,以查找需要添加哪些 ID 以及需要删除哪些 ID。

使用 Automapper 时,请考虑使用

ProjectTo
方法,而不是使用急切加载的关联来加载实体,调用
Map
,然后迭代关系。这可以产生更有效的查询,减少 JOIN 产生的笛卡尔积的大小。

© www.soinside.com 2019 - 2024. All rights reserved.