试图模拟 IQueryable 实体框架查询

Posted

技术标签:

【中文标题】试图模拟 IQueryable 实体框架查询【英文标题】:Trying to Mock IQueryable Entity Framework Query 【发布时间】:2018-10-11 18:04:55 【问题描述】:

我正在尝试测试查询是否不区分大小写。此生产代码有效:

public ILookup<string, EEntry> GetEEntries(int batchId, List<string> employeeIds)

  using (WithNoLock())
  
    var result = from e in _entities.EEntries
                 where e.CPayBatchProcessId == batchId
                       && (!e.Blocked.HasValue || e.Blocked.Value != true)
                       && employeeIds.Contains(e.Id)
                 select e;

    return result.ToLookup(e => e.Id, StringComparer.OrdinalIgnoreCase);
  

我无法让单元测试工作。我的第一次尝试失败了,因为我相信列表是IEnumerable 而不是IQueryable。但是,我在IQueryable 的尝试没有通过。该查询区分大小写,我不希望这样。这就是我所做的IQueryable

[TestCase("abc", "ABC")]
public void EEntriesAreCaseInsensitive(string employeeId1Input, string employeeId1Output)

  var payEntries = new List<EEntry>
  
    new EEntry() CPayBatchProcessId = 8, Id = employeeId1Input,
    new EEntry() CPayBatchProcessId = 8, Id = "123"
  .AsQueryable();

  var payEntriesDbSet = new Mock<DbSet<EEntry>>();
  payEntriesDbSet.As<IQueryable<EEntry>>().Setup(x => x.Provider).Returns(payEntries.Provider);
  payEntriesDbSet.As<IQueryable<EEntry>>().Setup(x => x.Expression).Returns(payEntries.Expression);
  payEntriesDbSet.As<IQueryable<EEntry>>().Setup(x => x.ElementType).Returns(payEntries.ElementType);
  payEntriesDbSet.As<IQueryable<EEntry>>().Setup(x => x.GetEnumerator()).Returns(payEntries.GetEnumerator);

  var context = new Mock<ISomeContext>();
  context.Setup(x => x.EEntries).Returns(payEntriesDbSet.Object);

  var employeeIds = new List<string>()  "aBc", "dEf", "gHi" ;

  var repo = new EEntriesRepository(context.Object);
  var payEntryRecords = repo.GetEEntries(8, employeeIds);

  Assert.IsTrue(payEntryRecords.Contains(employeeId1Output));

我错过了什么?

注意:EEntry.Id 的 getter 返回 .ToUpper()。生产代码正确地忽略了这一点。测试代码没有。

【问题讨论】:

不管.Contains() 使用List 实现吗?哪个返回false。更改它以匹配您的生产代码:employeeIds.Contains(employeeId1Output, StringComparer.OrdinalIgnoreCase) 返回 true。 实际运行时,针对数据库,查询语法会转换为:([Extent1].[id] IN ('aBc', 'dEf', 'gHi'))。这会针对不区分大小写的数据库运行。 我缺少的可能是查询语法不会仅仅因为它是 IQueryable 而被转换为 SQL。在单元测试期间,一切都在内存中。也许我在期待一些不可能的事情。 如果您返回 IEnumerable 并执行 var payEntryRecords = repo.GetEEntries(8, employeeIds).ToList(); 会发生什么?那么忽略大小写的.Contains 重载是否有效? 我认为它刚刚击中了我。我正在尝试测试查询中是否区分大小写,但这并不是我真正应该测试的。这就像在单元测试中测试 SQL Server 功能一样。在单元测试中,我需要做的是确保查询返回数据(考虑区分大小写),然后测试 ILookup 在忽略大小写的情况下返回值。 【参考方案1】:

您似乎没有在任何地方使用employeeId1Output,但您断言它应该在payEntryRecords 中。根据您的代码,您似乎应该断言 employeeId1Input 是否在 payEntryRecords 中。您明确地将 employeeId1Input 添加到您的 payEntries 对象中。

【讨论】:

employeeId1Output 是有意为输入的不同大小写。我想断言 ID 在那里,不管它的情况如何。如果我将第一个employeeId 更改为“ABC”(而不是“aBc”),测试就会通过。 嗯,这是.contains 方法的问题,而不是您的查询。 @mxmissile 关于过载是正确的。 This 问题也可能有帮助 我实际上尝试了该链接的建议,但 SQL Server 无法识别并失败。【参考方案2】:

字符串比较不区分大小写的性质取决于您的数据库提供商。 SQL Server 将比较字符串不区分大小写,而我相信 PostgreSQL 之类的则不会。

因此,如果您的代码逻辑执行employeeIds.Contains(e.Id),EF 会将等效逻辑传递给数据库。当用 Mock 和 List&lt;T&gt;.AsQueryable() 替换它时,C# 会将字符串视为区分大小写,而 SQL Server 不关心。 IN() 子句和字符串比较不区分大小写。

我会考虑使用以下或类似的方法来确保字符串比较不区分大小写。这应该适用于数据库提供程序,并且可以更好地处理模拟数据。

public ILookup<string, EEntry> GetEEntries(int batchId, List<string> employeeIds)

  if(employeeIds == null) throw new ArgumentNullException("employeeIds");

  employeeIds = employeeIds.ConvertAll(x => x.ToUpper());
  using (WithNoLock())
  
    var result = from e in _entities.EEntries
                 where e.CPayBatchProcessId == batchId
                       && (!e.Blocked.HasValue || e.Blocked.Value != true)
                       && employeeIds.Contains(e.Id.ToUpper())
                 select e;

    return result.ToLookup(e => e.Id, StringComparer.OrdinalIgnoreCase);
  

【讨论】:

我喜欢这样,除了一件事:原始代码可以在 SQL Server 上正常工作。它只是不适用于单元测试。理想情况下,我不应该仅仅为了让单元测试工作而更改生产代码。我认为真正的答案是只测试ILookup 返回对象,这就是我最终要做的。

以上是关于试图模拟 IQueryable 实体框架查询的主要内容,如果未能解决你的问题,请参考以下文章

实体框架,如何将 IQueryable 与多个 where 转换为 SQL 一起使用?

我正在寻找一种从字符串构建 IQueryable 查询的方法

实体框架代码优先 IQueryable

在接口上模拟扩展方法

IQueryable 实体框架 POCO 映射

FakeItEasy DbSet / IQueryable<T> - 实体框架 6