C# 如何模拟LINQ到实体帮助程序,如';SqlFunctions.StringConvert()';

C# 如何模拟LINQ到实体帮助程序,如';SqlFunctions.StringConvert()';,c#,unit-testing,entity-framework-4,mocking,moq,C#,Unit Testing,Entity Framework 4,Mocking,Moq,我正在使用EF 4,并尝试使用Moq对以下行进行单元测试: var convertError = models .Where(x => SqlFunctions.StringConvert((decimal?) (x.convert ?? 0)) == "0") .Any(); 如果检测到上下文被模拟,则似乎SqlFunctions.StringConvert()将抛出 它给出了一个错误的说法: 此函数只能从LINQ调用到实体 是否可以

我正在使用EF 4,并尝试使用Moq对以下行进行单元测试:

var convertError = models
             .Where(x => SqlFunctions.StringConvert((decimal?) (x.convert ?? 0)) == "0")
             .Any();
如果检测到上下文被模拟,则似乎
SqlFunctions.StringConvert()
将抛出

它给出了一个错误的说法:

此函数只能从LINQ调用到实体


是否可以告诉
SqlFunctions.StringConvert
返回一个模拟对象,以便消除此错误?

不可能,因为函数的实现如下所示:

[EdmFunction("SqlServer", "STR")]
public static string StringConvert(decimal? number, int? length)
{
    throw EntityUtil.NotSupported(Strings.ELinq_EdmFunctionDirectCall);
}
您不能使用Moq来伪造此函数。您需要更强大的模拟框架,它将允许您替换静态函数调用-可能是Microsoft Fakes、TypeMock Isolator或JustMock

或者您需要考虑您的测试方法,因为模仿上下文是错误的想法。相反,您应该有如下内容:

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

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

您可以模拟EDM函数,我使用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);
   }
公共静态类EdmxExtensions
{
[EdmFunction(“SqlServer”、“STR”)]
公共静态字符串StringConvert(十进制数,整数长度)
{
抛出EntityUtil.NotSupported(Strings.elink\u EdmFunctionDirectCall);
}
公共静态IQueryable MyFunction(此IDbContext上下文、十进制数、整数长度)
{
context.Person.Where(s=>StringConvert(s.personId,number,length);
}
然后您将能够模拟MyFunction,因为它是一个接口可用的方法,并且EntityFramework在您尝试调用它时不会生气


我没有在Moq中尝试过这一点,但您可以用类似的方式来实现。

另一种方法是,您可以编写自己的方法,该方法具有相同的属性标记和方法签名,然后实际实现方法单元测试目的,而不是抛出异常。实体框架忽略函数中的代码,因此它将ver调用它。

我所做的是提供我自己的DbFunctions实现,这样单元测试中的LINQ to对象使用一个简单的.NET实现,LINQ to EF在运行时使用DbFunctionAttribute的方式与System.Data.Entity.DbFunctions相同。我曾想过模拟DbFunctions,但嘿,LINQ to对象实现ons非常有用且工作正常。以下是一个示例:

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));
    }
}

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

像这样创建一个接口,并确保包含属性

public interface ISqlFunctions
{
    [System.Data.Entity.Core.Objects.DataClasses.EdmFunction("SqlServer", "STR")]
    string StringConvert(Decimal? number);
}
然后编写facade类。这应该是实现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();
    }
实体框架将以与
SqlFunctions.StringConvert(decimal?
相同的方式在接口上使用该属性

在单元测试中,您可以为被测系统提供facade类或接口模拟。

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

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

    [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;
    }

谢谢。我不知道TypeMock和JustMock截取CLR。这很酷:)。它们都是商业性的,所以我会看看是否可以改为重构。最好是编写集成测试,一直到DB并进行测试。