单元测试Mock EF.Property<T>时遇异常,求正确Mock配置方案
问题:EF.Property在单元测试Mock时抛出异常
背景代码
泛型类中实现的查询方法:
public virtual List<TEntity> GetExistingRecords(HashSet<TKey> sourceKeys) { var propertyName = ((MemberExpression)KeySelector().Body).Member.Name; return _targetDbContext.Set<TEntity>() .Where(e => sourceKeys.Contains(EF.Property<TKey>(e, propertyName))) .ToList(); }
派生类中实现的KeySelector:
protected override Expression<Func<AccountEntity, long>> KeySelector() => accountEntity => accountEntity.AccountNumber;
单元测试代码
测试类及两个测试方法:
public class CustomerServiceTests { private readonly Mock<WriteDbContext> _mockCustomerDbContext; private readonly Mock<StagingDbContext> _mockStagingDbContext; private readonly Mock<IMapper> _mockMapper; private readonly Core.Concrete.Domain.CustomerService _customerService; public CustomerServiceTests() { _mockCustomerDbContext = new Mock<WriteDbContext>(); _mockStagingDbContext = new Mock<StagingDbContext>(); _mockMapper = new Mock<IMapper>(); _customerService = new Core.Concrete.Domain.CustomerService(_mockStagingDbContext.Object, _mockCustomerDbContext.Object, _mockMapper.Object); } [Fact] public async Task MigrateAccounts_ShouldAddNewAccounts() { // Arrange var sourceAccounts = CustomerTestData.GenerateAccounts(1); var sourceAccountEntities = CustomerTestData.GenerateAccountEntities(1); var existingAccounts = new List<AccountEntity>().AsQueryable(); _mockMapper.Setup(m => m.Map<List<Account>>(It.IsAny<List<AccountDto>>())).Returns(sourceAccounts); _mockMapper.Setup(m => m.Map<List<AccountEntity>>(It.IsAny<List<Account>>())).Returns(sourceAccountEntities); _mockCustomerDbContext.Setup(db => db.Set<AccountEntity>()).ReturnsDbSet(existingAccounts); _mockCustomerDbContext.Setup(db => db.Set<AccountEntity>().AddRangeAsync(It.IsAny<List<AccountEntity>>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask); _mockCustomerDbContext.Setup(db => db.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1); // Act await _customerService.MigrateAccounts(); // Assert _mockCustomerDbContext.Verify(db => db.Set<AccountEntity>().AddRangeAsync(It.Is<List<AccountEntity>>(entities => entities.Count == 1), It.IsAny<CancellationToken>()), Times.Once); _mockCustomerDbContext.Verify(db => db.Set<AccountEntity>().UpdateRange(It.IsAny<List<AccountEntity>>()), Times.Never); _mockCustomerDbContext.Verify(db => db.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once); } [Fact] public async Task MigrateAccounts_ShouldUpdateExistingAccounts() { // Arrange var sourceAccounts = CustomerTestData.GenerateAccounts(1); var existingAccounts = CustomerTestData.GenerateAccountEntities(1).AsQueryable(); _mockMapper.Setup(m => m.Map<List<Account>>(It.IsAny<List<AccountDto>>())).Returns(sourceAccounts); _mockCustomerDbContext.Setup(db => db.Set<AccountEntity>()).ReturnsDbSet(existingAccounts); _mockCustomerDbContext.Setup(db => db.Set<AccountEntity>().UpdateRange(It.IsAny<List<AccountEntity>>())); _mockCustomerDbContext.Setup(db => db.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1); _mockCustomerDbContext.Setup(db => db.ReplaceEntity(It.IsAny<AccountEntity>(), It.IsAny<AccountEntity>())); // Act await _customerService.MigrateAccounts(); // Assert _mockCustomerDbContext.Verify(db => db.Set<AccountEntity>().AddRangeAsync(It.IsAny<IEnumerable<AccountEntity>>(), It.IsAny<CancellationToken>()), Times.Never); _mockCustomerDbContext.Verify(db => db.Set<AccountEntity>().UpdateRange(It.Is<List<AccountEntity>>(entities => entities.Count == 1)), Times.Once); _mockCustomerDbContext.Verify(db => db.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once); _mockCustomerDbContext.Verify(db => db.ReplaceEntity(It.IsAny<AccountEntity>(), It.IsAny<AccountEntity>()), Times.Exactly(1)); } }
错误现象
第一个测试(无现有记录)正常通过,但第二个测试(有现有记录)抛出异常:
System.InvalidOperationException : The EF.Property<T> method may only be used within Entity Framework LINQ queries.
异常发生在GetExistingRecords方法的Where查询执行阶段。
解决方案
方法1:修改业务代码,避免直接使用EF.Property
问题根源是:真实EF Core能解析EF.Property表达式并转换为SQL,但Mock的内存集合用的是LINQ to Objects,无法识别该方法。我们可以利用已有的KeySelector表达式构建查询逻辑,让普通LINQ也能处理:
修改GetExistingRecords方法:
public virtual List<TEntity> GetExistingRecords(HashSet<TKey> sourceKeys) { var keySelector = KeySelector(); // 构建表达式:e => sourceKeys.Contains(keySelector(e)) var parameter = keySelector.Parameters[0]; var containsCall = Expression.Call( typeof(HashSet<TKey>), nameof(HashSet<TKey>.Contains), Type.EmptyTypes, Expression.Constant(sourceKeys), keySelector.Body ); var whereLambda = Expression.Lambda<Func<TEntity, bool>>(containsCall, parameter); return _targetDbContext.Set<TEntity>() .Where(whereLambda) .ToList(); }
这种修改不需要依赖额外库,同时兼容真实EF和内存集合的LINQ查询,单元测试的Mock可以正常执行。
方法2:使用支持EF函数的Mock库
如果不想修改业务代码,可以使用Moq.EntityFrameworkCore这类专门为EF Core设计的Mock库,它能正确解析EF特有的表达式(包括EF.Property)。只需将原来的ReturnsDbSet替换为该库提供的扩展方法即可。
内容的提问来源于stack exchange,提问作者blawford
相关产品推荐
相关产品推荐

