【问题标题】:How to mock LINQ to Entities helpers such as 'SqlFunctions.StringConvert()'如何模拟 LINQ to Entities 助手,例如“SqlFunctions.StringConvert()”
【发布时间】:2020-09-08 22:47:46
【问题描述】:

我正在使用 EF 4 并尝试使用 Moq 对后续行进行单元测试:

var convertError = models
             .Where(x => SqlFunctions.StringConvert((decimal?) (x.convert ?? 0)) == "0")
             .Any();

如果SqlFunctions.StringConvert() 检测到上下文被模拟,它似乎会抛出。

它给出了一个错误提示:

此函数只能从 LINQ to Entities 调用

是否可以告诉SqlFunctions.StringConvert 返回一个模拟对象,这样我就可以摆脱这个错误?

【问题讨论】:

  • 最好的办法是编写集成测试,一直到 DB 并以这种方式进行测试。

标签: c# unit-testing entity-framework-4 mocking moq


【解决方案1】:

我所做的是提供我自己的 DbFunctions 实现,以便单元测试中的 LINQ To Objects 使用简单的 .NET 实现,而 LINQ To EF 在运行时使用 DbFunctionAttribute 的方式与 System.Data.Entity.DbFunctions 相同.我曾考虑过模拟 DbFunctions,但是嘿,LINQ to Objects 实现很有用并且工作正常。这是一个例子:

public static class DbFunctions
{
    [DbFunction("Edm", "AddMinutes")]
    public static TimeSpan? AddMinutes(TimeSpan? timeValue, int? addValue)
    {
        return timeValue == null ? (TimeSpan?)null : timeValue.Value.Add(new TimeSpan(0, addValue.Value, 0));
    }
}

【讨论】:

    【解决方案2】:

    不,这是不可能的,因为函数的实现看起来像:

    [EdmFunction("SqlServer", "STR")]
    public static string StringConvert(decimal? number, int? length)
    {
        throw EntityUtil.NotSupported(Strings.ELinq_EdmFunctionDirectCall);
    }
    

    您不能使用最小起订量来伪造此功能。您需要更强大的模拟框架,它可以让您替换静态函数调用 - 可能是 Microsoft Fakes、TypeMock Isolator 或 JustMock。

    或者您需要考虑您的测试方法,因为模拟上下文是错误的想法。你应该有类似的东西:

    var convertError = myQueryProvider.ConvertQuery(x.convert); 
    

    queryProvider 将是隐藏查询的可模拟类型。查询是与数据库相关的逻辑,应该针对真实数据库进行测试。查询周围的代码是您的应用程序逻辑,它应该进行单元测试 - 正确测试它们的最佳解决方案是通过一些接口将它们分开(在这种情况下是查询提供程序,但人们通常使用完整的特定存储库)。这个原则来自关注点分离——查询执行是独立关注点,因此它被放置在单独测试的自己的方法中。

    【讨论】:

    • 谢谢。我不知道 TypeMock 和 JustMocks 拦截了 CLR。这很酷 :)。它们都是商业的,所以我会看看我是否可以重构。
    【解决方案3】:

    您可以模拟 EdmFunctions,我使用 NSubstitute(它也不支持模拟静态函数)完成了这项工作。诀窍是将您的 DbContext 包装在一个接口中。然后,将您的静态 EdmFunction 函数添加到静态类,并在静态类中为您的上下文创建扩展方法以调用该方法。例如

    public static class EdmxExtensions
    {
       [EdmFunction("SqlServer", "STR")]
       public static string StringConvert(decimal? number, int? length)
       {
          throw EntityUtil.NotSupported(Strings.ELinq_EdmFunctionDirectCall);
       }
    
       public static IQueryable<Person> MyFunction(this IDbContext context, decimal? number, int? length)
       {
          context.Person.Where(s => StringConvert(s.personId, number, length);
       }
    

    然后您将能够模拟 MyFunction,因为它是接口可用的方法,并且 EntityFramework 在您尝试调用它时不会生气。

    我没有在起订量上尝试过这个,但你可以用类似的方式来做这个。

    【讨论】:

      【解决方案4】:

      另一种方法,您可以编写自己的方法,具有相同的属性标签和方法签名,然后实际实现方法单元测试目的,而不是抛出异常。 Entity Framework 会忽略函数中的代码,所以它永远不会调用它。

      【讨论】:

        【解决方案5】:

        你不能告诉SqlFunctions.StringConvert 返回一个模拟对象,因为它是一个静态方法。但是你可以为它创建一个接口并创建一个外观类。

        像这样创建一个界面并确保包含属性

        public interface ISqlFunctions
        {
            [System.Data.Entity.Core.Objects.DataClasses.EdmFunction("SqlServer", "STR")]
            string StringConvert(Decimal? number);
        }
        

        然后编写你的外观类。这应该是您希望 Linq to Entity 做的任何事情的 C# 方式。

        public class SqlFunctionsFacade : ISqlFunctions
        {
            public string StringConvert(decimal? number)
            {
                return number?.ToString();
            }
        }
        

        在你的实现中,在你的 linq 查询中使用你的接口

            public SomethingOrOther(ISqlFunctions sqlFunctions)
            {
                var convertError = models
                    .Where(x => sqlFunctions.StringConvert((decimal?)(x.convert ?? 0)) == "0")
                    .Any();
            }
        

        Entity Framework 将像在SqlFunctions.StringConvert(decimal?) 上一样使用接口上的属性。

        在你的单元测试中,你可以为你的被测系统提供你的外观类或接口的模拟。

        【讨论】:

          【解决方案6】:

          使用 System.Data.Entity.DbFunctionAttribute 并创建 EF DbFunction 的“存根”实现。然后,当您运行应用程序时,它将使用 EF 实现,当您运行单元测试时,您的“存根”实现就会发挥作用。

          “存根”实现最接近 MSSQL 中的 Str() 使用新的 System.Data.Entity.DbFunctionAttribute 。 对于那些不喜欢浪费时间重新发明轮子但为了单元测试而需要它的人。享受吧。

              [DbFunction("SqlServer", "STR")]
              public static string StringConvert(double? number, int? length, int? decimalArg)
              {
                  if (number == null)
                      return null;
          
                  var roundedValue = decimalArg != null 
                          ? Math.Round(number.Value, decimalArg.Value).ToString($"##.{new string('0', decimalArg.Value)}") 
                          : number.Value.ToString(CultureInfo.InvariantCulture); 
                  
                  return length != null && length - roundedValue.Length > 0
                      ? $"{roundedValue}{new string(' ', length.Value - roundedValue.Length)}"
                      : roundedValue;
              }
          

          【讨论】:

            猜你喜欢
            • 2019-10-28
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-06-10
            相关资源
            最近更新 更多