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

单元测试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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 11:27:09