2012-03-06 125 views
29

我使用System.Data.Objects.EntityFunctions.TruncateTime方法来得到我的查询日期时间的日期部分:EntityFunctions.TruncateTime和单元测试

if (searchOptions.Date.HasValue) 
    query = query.Where(c => 
     EntityFunctions.TruncateTime(c.Date) == searchOptions.Date); 

这种方法(我相信同样适用于其他EntityFunctions方法)无法执行外的LINQ to Entities。在单元测试,从而有效地是LINQ到对象执行该代码时,使NotSupportedException被抛出:

System.NotSupportedException:该函数只能从 LINQ调用实体。

我使用的是存根在我的测试中假DbSets库。

那么我应该如何测试我的查询?

+0

我已经删除了我的答案,这是不是对您有用。不知何故,我怀疑我没有告诉你任何新东西。我不知道如何在单元测试中处理您的查询,除非将整个查询放在单元测试中实现了LTO友好的接口('c => c.Date.Date == ...') 。 – Slauma 2012-03-06 16:13:48

+0

您能否取消删除您的答案?我认为这是相当有效的,可以帮助其他人...... – 2012-03-06 16:19:04

+0

该方法只是一个占位符。如果Linq to Entity转换器在看到此方法时处理表达式树,它知道如何用数据库特定的构造来替换它。因此该方法本身没有任何实现,但会抛出NotSupportedException。 – Pawel 2012-03-06 17:16:58

回答

18

您不能 - 如果单元测试意味着您在内存中使用虚假存储库,并且因此使用LINQ to Objects。如果你使用LINQ to Objects来测试你的查询,你没有测试你的应用程序,而只测试你的假库。

你的例外是不太危险的情况,因为它表明你有一个红色测试,但可能实际上是一个工作应用程序。

更危险的是相反的情况:有一个绿色测试,但一个崩溃的应用程序或查询不会返回与您的测试相同的结果。查询像...

context.MyEntities.Where(e => MyBoolFunction(e)).ToList() 

context.MyEntities.Select(e => new MyEntity { Name = e.Name }).ToList() 

...将正常工作在您的测试,但不与LINQ到应用程序中的实体。

查询像...

context.MyEntities.Where(e => e.Name == "abc").ToList() 

...可能会使用LINQ到对象比LINQ返回不同的结果实体。

您只能通过构建使用您的应用程序的LINQ to Entities提供程序和实际数据库的集成测试来测试此问题以及您的问题中的查询。

编辑

如果你仍然想编写单元测试,我想你一定假查询本身或在查询中至少表达式。我可以想象,沿着下面的代码线的东西可能工作:

Where表达创建的接口:

public interface IEntityExpressions 
{ 
    Expression<Func<MyEntity, bool>> GetSearchByDateExpression(DateTime date); 
    // maybe more expressions which use EntityFunctions or SqlFunctions 
} 

为应用程序创建一个实现......

public class EntityExpressions : IEntityExpressions 
{ 
    public Expression<Func<MyEntity, bool>> 
     GetSearchByDateExpression(DateTime date) 
    { 
     return e => EntityFunctions.TruncateTime(e.Date) == date; 
     // Expression for LINQ to Entities, does not work with LINQ to Objects 
    } 
} 

...并在单元测试项目的第二个实施:

public class FakeEntityExpressions : IEntityExpressions 
{ 
    public Expression<Func<MyEntity, bool>> 
     GetSearchByDateExpression(DateTime date) 
    { 
     return e => e.Date.Date == date; 
     // Expression for LINQ to Objects, does not work with LINQ to Entities 
    } 
} 

在你的类,你正在使用的查询创建这个接口和一个私人会员两个构造函数:

public class MyClass 
{ 
    private readonly IEntityExpressions _entityExpressions; 

    public MyClass() 
    { 
     _entityExpressions = new EntityExpressions(); // "poor man's IOC" 
    } 

    public MyClass(IEntityExpressions entityExpressions) 
    { 
     _entityExpressions = entityExpressions; 
    } 

    // just an example, I don't know how exactly the context of your query is 
    public IQueryable<MyEntity> BuildQuery(IQueryable<MyEntity> query, 
     SearchOptions searchOptions) 
    { 
     if (searchOptions.Date.HasValue) 
      query = query.Where(_entityExpressions.GetSearchByDateExpression(
       searchOptions.Date)); 
     return query; 
    } 
} 

使用第一个(默认)构造函数在您的应用程序:

var myClass = new MyClass(); 
var searchOptions = new SearchOptions { Date = DateTime.Now.Date }; 

var query = myClass.BuildQuery(context.MyEntities, searchOptions); 

var result = query.ToList(); // this is LINQ to Entities, queries database 

使用与FakeEntityExpressions第二构造在单元测试:

IEntityExpressions entityExpressions = new FakeEntityExpressions(); 
var myClass = new MyClass(entityExpressions); 
var searchOptions = new SearchOptions { Date = DateTime.Now.Date }; 
var fakeList = new List<MyEntity> { new MyEntity { ... }, ... }; 

var query = myClass.BuildQuery(fakeList.AsQueryable(), searchOptions); 

var result = query.ToList(); // this is LINQ to Objects, queries in memory 

如果您正在使用依赖注入容器,你可以通过注射,如果IEntityExpressions相应的执行到构造利用它,不需要默认的构造函数。

我已经测试了上面的示例代码,它工作。

+3

我明白L2O和L2E之间的区别 - 我知道我的测试没有完全复制SQL服务器的行为,但我仍然可以测试大量服务。我很高兴可能出现假阳性结果 - 如果发生这种情况,我可以对测试进行微调。他们中有99%的工作有利于风险。 – 2012-03-06 15:05:04

+1

感谢您的编辑。我担心需要这样的黑客;-)这是一个可惜的EF是一半... – 2012-03-07 12:24:06

15

您可以定义一个新的静态函数(你可以把它作为一个扩展方法,如果你想):

[EdmFunction("Edm", "TruncateTime")] 
    public static DateTime? TruncateTime(DateTime? date) 
    { 
     return date.HasValue ? date.Value.Date : (DateTime?)null; 
    } 

然后你就可以使用该功能在LINQ到实体和LINQ to对象,它会工作。但是,该方法意味着您必须将呼叫替换为EntityFunctions,并呼叫您的新班级。

另一个更好的(但更多参与)选项是使用表达式访问者,并为您的内存中的DbSets编写自定义提供程序,以便调用EntityFunctions以调用内存实现。

+0

它适用于我的情况,是迄今为止最简单的解决方案!谢谢。 – 2014-06-20 14:04:47

+0

最初的复杂问题的最佳解决方案。 – Anish 2015-11-11 15:48:22

+0

更好的扩展方法。 public static DateTime? TruncateTime(此日期时间?日期) 然后使用; myDate.TruncateTime() – tkerwood 2016-01-14 04:43:21

3

my answerHow to Unit Test GetNewValues() which contains EntityFunctions.AddDays function中所述,您可以使用查询表达式访问者用您自己的LINQ To Objects兼容实现替换对EntityFunctions函数的调用。

的实施将看起来像:

using System; 
using System.Data.Objects; 
using System.Linq; 
using System.Linq.Expressions; 

static class EntityFunctionsFake 
{ 
    public static DateTime? TruncateTime(DateTime? original) 
    { 
     if (!original.HasValue) return null; 
     return original.Value.Date; 
    } 
} 
public class EntityFunctionsFakerVisitor : ExpressionVisitor 
{ 
    protected override Expression VisitMethodCall(MethodCallExpression node) 
    { 
     if (node.Method.DeclaringType == typeof(EntityFunctions)) 
     { 
      var visitedArguments = Visit(node.Arguments).ToArray(); 
      return Expression.Call(typeof(EntityFunctionsFake), node.Method.Name, node.Method.GetGenericArguments(), visitedArguments); 
     } 

     return base.VisitMethodCall(node); 
    } 
} 
class VisitedQueryProvider<TVisitor> : IQueryProvider 
    where TVisitor : ExpressionVisitor, new() 
{ 
    private readonly IQueryProvider _underlyingQueryProvider; 
    public VisitedQueryProvider(IQueryProvider underlyingQueryProvider) 
    { 
     if (underlyingQueryProvider == null) throw new ArgumentNullException(); 
     _underlyingQueryProvider = underlyingQueryProvider; 
    } 

    private static Expression Visit(Expression expression) 
    { 
     return new TVisitor().Visit(expression); 
    } 

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression) 
    { 
     return new VisitedQueryable<TElement, TVisitor>(_underlyingQueryProvider.CreateQuery<TElement>(Visit(expression))); 
    } 

    public IQueryable CreateQuery(Expression expression) 
    { 
     var sourceQueryable = _underlyingQueryProvider.CreateQuery(Visit(expression)); 
     var visitedQueryableType = typeof(VisitedQueryable<,>).MakeGenericType(
      sourceQueryable.ElementType, 
      typeof(TVisitor) 
      ); 

     return (IQueryable)Activator.CreateInstance(visitedQueryableType, sourceQueryable); 
    } 

    public TResult Execute<TResult>(Expression expression) 
    { 
     return _underlyingQueryProvider.Execute<TResult>(Visit(expression)); 
    } 

    public object Execute(Expression expression) 
    { 
     return _underlyingQueryProvider.Execute(Visit(expression)); 
    } 
} 
public class VisitedQueryable<T, TExpressionVisitor> : IQueryable<T> 
    where TExpressionVisitor : ExpressionVisitor, new() 
{ 
    private readonly IQueryable<T> _underlyingQuery; 
    private readonly VisitedQueryProvider<TExpressionVisitor> _queryProviderWrapper; 
    public VisitedQueryable(IQueryable<T> underlyingQuery) 
    { 
     _underlyingQuery = underlyingQuery; 
     _queryProviderWrapper = new VisitedQueryProvider<TExpressionVisitor>(underlyingQuery.Provider); 
    } 

    public IEnumerator<T> GetEnumerator() 
    { 
     return _underlyingQuery.GetEnumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return GetEnumerator(); 
    } 

    public Expression Expression 
    { 
     get { return _underlyingQuery.Expression; } 
    } 

    public Type ElementType 
    { 
     get { return _underlyingQuery.ElementType; } 
    } 

    public IQueryProvider Provider 
    { 
     get { return _queryProviderWrapper; } 
    } 
} 

这里是TruncateTime用法示例:

var linq2ObjectsSource = new List<DateTime?>() { null }.AsQueryable(); 
var visitedSource = new VisitedQueryable<DateTime?, EntityFunctionsFakerVisitor>(linq2ObjectsSource); 
// If you do not use a lambda expression on the following line, 
// The LINQ To Objects implementation is used. I have not found a way around it. 
var visitedQuery = visitedSource.Select(dt => EntityFunctions.TruncateTime(dt)); 
var results = visitedQuery.ToList(); 
Assert.AreEqual(1, results.Count); 
Assert.AreEqual(null, results[0]); 
2

虽然我很喜欢使用EntityExpressions类由Smaula给出的答案,我认为它有点太多了。基本上,它将整个实体抛出该方法,进行比较并返回一个布尔值。

在我的情况下,我需要这个EntityFunctions.TruncateTime()来做一个group,所以我没有日期来比较,或者bool来返回,我只想得到正确的实现来获得日期部分。所以我写道:

private static Expression<Func<DateTime?>> GetSupportedDatepartMethod(DateTime date, bool isLinqToEntities) 
    { 
     if (isLinqToEntities) 
     { 
      // Normal context 
      return() => EntityFunctions.TruncateTime(date); 
     } 
     else 
     { 
      // Test context 
      return() => date.Date; 
     } 
    } 

在我的情况下,我不需要与两个独立实现的接口,但它应该是一样的。

我想分享这个,因为它尽可能做到最小的事情。它只选择正确的方法来获取日期部分。

1

我意识到这是一个古老的线程,但想发布一个答案反正。

下面的解决方案是使用Shims

我不知道做了什么版本(2013,2012,2010),也香精(简化版,专业,优质,最终)的Visual Studio的组合,让您使用垫片等等这可能是所有人都无法获得的。

这里是OP发布

// some method that returns some testable result 
public object ExecuteSomething(SearchOptions searchOptions) 
{ 
    // some other preceding code 

    if (searchOptions.Date.HasValue) 
     query = query.Where(c => 
      EntityFunctions.TruncateTime(c.Date) == searchOptions.Date); 

    // some other stuff and then return some result 
} 

的代码下面将设在一些单元测试项目和一些单元测试文件。这是使用Shims的单元测试。

// Here is the test method 
public void ExecuteSomethingTest() 
{ 
    // arrange 
    var myClassInstance = new SomeClass(); 
    var searchOptions = new SearchOptions(); 

    using (ShimsContext.Create()) 
    { 
     System.Data.Objects.Fakes.ShimEntityFunctions.TruncateTimeNullableOfDateTime = (dtToTruncate) 
      => dtToTruncate.HasValue ? (DateTime?)dtToTruncate.Value.Date : null; 

     // act 
     var result = myClassInstance.ExecuteSomething(searchOptions); 
     // assert 
     Assert.AreEqual(something,result); 
    } 
} 

我相信这可能是测试代码,使用EntityFunctions的不生成NotSupportedException异常最清洁和最非侵入性的方式。

+0

垫片需要Visual Studio终极版本ref:https://msdn.microsoft.com/en-us/library/hh549176.aspx – Rama 2015-07-15 07:58:09

+1

@DRAM在VS 2013的Premium版本(https:/ /msdn.microsoft.com/en-us/library/hh549175.aspx),这是我使用和我已经使用垫片。在Visual Studio 2015中,它们可以在企业版中使用(以前是高级版和终极版),我怀疑它们在专业版或社区版中可用,但不确定。 – Igor 2015-07-15 16:31:50

0

您还可以检查它以下列方式:

var dayStart = searchOptions.Date.Date; 
var dayEnd = searchOptions.Date.Date.AddDays(1); 

if (searchOptions.Date.HasValue) 
    query = query.Where(c => 
     c.Date >= dayStart && 
     c.Date < dayEnd);