EntityFunctions.TruncateTime和单元测试

时间:2012-03-06 14:05:30

标签: entity-framework unit-testing entity-framework-4 stub

我正在使用System.Data.Objects.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 to Entities。

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

那么我应该如何对我的查询进行单元测试?

6 个答案:

答案 0 :(得分:19)

你不能 - 如果单元测试意味着你在内存中使用假存储库,那么你正在使用LINQ to Objects。如果您使用LINQ to Objects测试查询,则不会测试您的应用程序,而只测试您的假存储库。

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

另一种情况则更危险:进行绿色测试,但崩溃的应用程序或查询不会返回与测试相同的结果。查询如......

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

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

...在您的测试中可以正常工作,但在应用程序中不能使用LINQ to Entities。

像......这样的查询。

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

...可能会使用LINQ to Objects而不是LINQ to Entities返回不同的结果。

您只能通过构建使用应用程序的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注入适当的实现到构造函数中来利用它,并且不需要默认构造函数。

我已经测试了上面的示例代码,但它确实有效。

答案 1 :(得分:15)

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

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

然后,您可以在LINQ to Entities和LINQ to Objects中使用该函数,它将起作用。但是,该方法意味着您必须通过调用新类来替换对EntityFunctions的调用。

另一个更好(但更复杂)的选项是使用表达式访问者,并为内存中的DbSet编写自定义提供程序,以通过调用内存实现来替换对EntityFunctions的调用。 / p>

答案 2 :(得分:3)

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

实施方式如下:

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

答案 3 :(得分:2)

虽然我喜欢Smaula使用EntityExpressions类给出的答案,但我认为它确实有点太多了。基本上,它会抛出整个实体,进行比较,然后返回一个bool。

在我的情况下,我需要这个EntityFunctions.TruncateTime()做一个分组,所以我没有比较的日期,或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;
        }
    } 

在我的情况下,我不需要具有两个单独实现的接口,但这应该是相同的。

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

答案 4 :(得分:1)

我意识到这是一个老话题,但无论如何都希望发布答案。

使用Shims

完成以下解决方案

我不确定哪些版本(2013年,2012年,2010年)以及Visual Studio的风格(快速,专业,高级,终极)组合允许您使用Shims,因此可能并非所有人都可以使用。< / p>

以下是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);
     }
}

我认为这可能是测试使用EntityFunction而不会产生NotSupportedException的代码的最干净,最具侵入性的方法。

答案 5 :(得分: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);