如何对基于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
相关产品推荐
相关产品推荐

