如何对包含EntityFunctions的GetNewValues()进行单元测试.AddDays函数

本文关键字:单元测试 函数 AddDays GetNewValues 包含 EntityFunctions | 更新日期: 2023-09-27 17:59:08

以下示例代码在生产中运行良好,但不能是单元代码测试,因为EntityFunctions。

我的单元测试项目正在使用InMemoryDatabase而不是真正的SQL数据库。我可以很容易地解决在SQL数据库中创建具有计算列的视图时出现的问题myValue和newValue。我想找到一种方法来完成单元测试工作不更改我的方法,也不创建新的SQL视图


public class EcaseReferralCaseRepository : Repository
{
        public class myType
        {
                public DateTime myValue;
                public DateTime newValue;
        }
        public myType GetNewValues()
        {
                return 
                        (myType)(from o in context.EcaseReferralCases
                        select new myType
                        {
                            // LINQ to Entity
                            myValue = (DateTime)System.Data.Objects.EntityFunctions.AddDays(o.StartDate, 0),
                            newValue = (DateTime)System.Data.Objects.EntityFunctions.AddDays(o.StartDate, 30)
                            // LINQ to Object
                            //myValue = o.StartDate.AddDays(0),
                            //newValue = o.StartDate.AddDays(30)
                        });
        }
}

这个链接展示了一个很好的单元测试EntityFunctions的例子,我用这种方法解决了我的一个单元测试困难,但不知道如何解决这个问题。

如何对包含EntityFunctions的GetNewValues()进行单元测试.AddDays函数

除非我弄错了,否则您将使用另一个IQueryable(可能是LINQ to Objects可查询源)切换EcaseReferralCases的实现。

最健壮的方法可能是使用表达式访问者将对EntityFunctions的调用替换为您自己的L2Objects兼容函数。

这是我的实现:

using System;
using System.Data.Objects;
using System.Linq;
using System.Linq.Expressions;
static class EntityFunctionsFake
{
    public static DateTime? AddDays(DateTime? original, int? numberOfDays)
    {
        if (!original.HasValue || !numberOfDays.HasValue)
        {
            return null;
        }
        return original.Value.AddDays(numberOfDays.Value);
    }
}
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> : IOrderedQueryable<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; }
    }
}

这里有一个用法示例:

var linq2ObjectsSource = new List<DateTime?>() { null }.AsQueryable();
var visitedSource = new VisitedQueryable<DateTime?, EntityFunctionsFakerVisitor>(linq2ObjectsSource);
var visitedQuery = visitedSource.Select(dt => EntityFunctions.AddDays(dt, 1));
var results = visitedQuery.ToList();
Assert.AreEqual(1, results.Count);
Assert.AreEqual(null, results[0]);

通过这种方式,您可以获得所有想要的特性:

  • 开发人员可以继续使用实体框架定义的标准EntityFunctions
  • 如果不在数据库上运行,生产实现仍然保证会引发异常
  • 查询可以针对伪造的存储库进行测试

而不是调用

System.Data.Objects.EntityFunctions.AddDays

我会直接注入一个自定义接口,它将调用转发到该方法,但随后可以出于测试目的对其进行模拟。

我确实喜欢按照Jean Hominal的建议实现ExpressionVisitor。在我的例子中,我的困难在于如何定义linq2ObjectsSource、visitedSource和visitedQuery。所以最后,我只需要为方法IQuerable GetSelectQuery(IQuerable query)创建一个接口,然后在Production and Test项目中有相应的类,该类是从该接口派生的,并实现了GetSelectQuery。它运行良好。

public interface IEntityFunctionsExpressions
{
   IQuerable<myType> GetSelectQuery(IQuerable<EcaseReferralCase> query); 
}

生产项目:

public class EntityFunctionsExpressions : IEntityFunctionsExpressions
{
    public EntityFunctionsExpressions()
    {
    }
    public IQuerable<myType> GetSelectQuery(IQuerable<EcaseReferralCase> query)
    {
        // Expression for LINQ to Entities, does not work with LINQ to Objects
        return 
                    (myType)(from o in query
                    select new myType
                    {
                        // LINQ to Entity
                        myValue = (DateTime)System.Data.Objects.EntityFunctions.AddDays(o.StartDate, 0),
                        newValue = (DateTime)System.Data.Objects.EntityFunctions.AddDays(o.StartDate, 30)
                    });
    }
}

单元内测试项目:

public class MockEntityFunctionsExpressions : IEntityFunctionsExpressions
{
    public MockEntityFunctionsExpressions()
    {
    }
    public IQuerable<myType> GetSelectQuery(IQuerable<EcaseReferralCase> query)
    {
        // Expression for LINQ to Objects, does not work with LINQ to Entities
        return 
                    (myType)(from o in query
                    select new myType
                    {
                        // LINQ to Object
                        myValue = o.StartDate.AddDays(0),
                        newValue = o.StartDate.AddDays(30)
                    });
    }
}

然后重写GetNewValues()方法:

public myType GetNewValues(){返回myrepository。EntityFunctionsExpression。GetSelectQuery(context.EcaseReferralCases);

}