C# 如何在单元测试中使用Moq和DBFunction来防止NotSupportedException?

C# 如何在单元测试中使用Moq和DBFunction来防止NotSupportedException?,c#,entity-framework,unit-testing,moq,C#,Entity Framework,Unit Testing,Moq,我目前正在尝试对通过实体框架运行的查询运行一些单元测试。查询本身在实时版本上运行时没有任何问题,但是单元测试总是失败 我已经将这一点缩小到使用DbFunctions.TruncateTime,但我不知道如何让单元测试反映实时服务器上发生的情况 以下是我正在使用的方法: public System.Data.DataTable GetLinkedUsers(int parentUserId) { var today = DateTime.Now.Date;

我目前正在尝试对通过实体框架运行的查询运行一些单元测试。查询本身在实时版本上运行时没有任何问题,但是单元测试总是失败

我已经将这一点缩小到使用DbFunctions.TruncateTime,但我不知道如何让单元测试反映实时服务器上发生的情况

以下是我正在使用的方法:

    public System.Data.DataTable GetLinkedUsers(int parentUserId)
    {
        var today = DateTime.Now.Date;

        var query = from up in DB.par_UserPlacement
                    where up.MentorId == mentorUserId
                        && DbFunctions.TruncateTime(today) >= DbFunctions.TruncateTime(up.StartDate)
                        && DbFunctions.TruncateTime(today) <= DbFunctions.TruncateTime(up.EndDate)
                    select new
                    {
                        up.UserPlacementId,
                        up.Users.UserId,
                        up.Users.FirstName,
                        up.Users.LastName,
                        up.Placements.PlacementId,
                        up.Placements.PlacementName,
                        up.StartDate,
                        up.EndDate,
                    };

        query = query.OrderBy(up => up.EndDate);

        return this.RunQueryToDataTable(query);
    }
public System.Data.DataTable GetLinkedUsers(int-parentUserId)
{
var today=DateTime.Now.Date;
var query=在DB.par\u UserPlacement中从上到下
其中up.MentorId==mentorUserId
&&DbFunctions.TruncateTime(今天)>=DbFunctions.TruncateTime(up.StartDate)
&&DbFunctions.TruncateTime(今天)up.EndDate);
返回此.RunQueryToDataTable(查询);
}
如果我在中注释掉带有DbFunctions的行,测试都会通过(除了那些检查是否只运行给定日期的有效结果的测试)

有没有办法提供DbFunctions.TruncateTime的模拟版本以用于这些测试?实际上,它应该只是返回Datetime.Date,但在EF查询中不可用

编辑:以下是使用日期检查失败的测试:

    [TestMethod]
    public void CanOnlyGetCurrentLinkedUsers()
    {
        var up = new List<par_UserPlacement>
        {
            this.UserPlacementFactory(1, 2, 1), // Create a user placement that is current
            this.UserPlacementFactory(1, 3, 2, false) // Create a user placement that is not current
        }.AsQueryable();

        var set = DLTestHelper.GetMockSet<par_UserPlacement>(up);

        var context = DLTestHelper.Context;
        context.Setup(c => c.par_UserPlacement).Returns(set.Object);

        var getter = DLTestHelper.New<LinqUserGetLinkedUsersForParentUser>(context.Object);

        var output = getter.GetLinkedUsers(1);

        var users = new List<User>();
        output.ProcessDataTable((DataRow row) => students.Add(new UserStudent(row)));

        Assert.AreEqual(1, users.Count);
        Assert.AreEqual(2, users[0].UserId);
    }
[TestMethod]
public void CanolyGetCurrentLinkedUsers()
{
var up=新列表
{
this.UserPlacementFactory(1,2,1),//创建当前的用户位置
this.UserPlacementFactory(1,3,2,false)//创建一个非当前的用户位置
}.AsQueryable();
var set=DLTestHelper.GetMockSet(up);
var context=DLTestHelper.context;
Setup(c=>c.par\u UserPlacement).Returns(set.Object);
var getter=DLTestHelper.New(context.Object);
var输出=getter.GetLinkedUsers(1);
var users=新列表();
output.ProcessDataTable((DataRow行)=>students.Add(newuserstudent(row));
Assert.AreEqual(1,users.Count);
Assert.AreEqual(2,用户[0].UserId);
}
编辑2:这是有关测试的消息和调试跟踪:

Test Result: Failed

Message: Assert.AreEqual failed. Expected:<1>. Actual:<0>

Debug Trace: This function can only be invoked from LINQ to Entities
测试结果:失败
消息:Assert.AreEqual失败。预期:。实际:
调试跟踪:此函数只能从LINQ调用到实体

从我所读到的内容来看,这是因为这个地方没有一个可以用于单元测试的该方法的LINQ to Entities实现,尽管在实时版本上有(因为它正在查询SQL server)。

嗯,不确定,但您不能这样做吗

context.Setup(s => DbFunctions.TruncateTime(It.IsAny<DateTime>()))
    .Returns<DateTime?>(new Func<DateTime?,DateTime?>(
        (x) => {
            /*  whatever modification is required here */
            return x; //or return modified;
        }));
context.Setup(s=>DbFunctions.TruncateTime(It.IsAny()))
.Returns(新函数)(
(x) =>{
/*这里需要做什么修改*/
return x;//或return modified;
}));
查看以下答案:

老实说,考虑到这一点,我完全同意答案,并且通常遵循以下原则:我的EF查询是针对数据库进行测试的,只有我的应用程序代码是使用Moq进行测试的

使用Moq测试上面的查询的EF查询,似乎没有一个优雅的解决方案,尽管有一些黑客的想法。例如,下面的答案。两个看起来都可以为你工作

另一种测试查询的方法是在我工作过的另一个项目上实现的方法:使用VS开箱即用单元测试,每个查询(再次重构为自己的方法)测试将包装在一个事务范围中。然后,项目的测试框架将负责手动将虚假数据输入数据库,查询将尝试过滤这些虚假数据。最后,事务从未完成,因此它被回滚由于事务作用域的性质,对于许多项目来说,这可能不是一个理想的方案。很可能不是在产品环境中


否则,如果你必须继续模仿功能,你可能想考虑其他嘲笑框架。

< P>谢谢所有的帮助,我设法找到了一个解决问题的方法。在添加EntityFramework的假程序集后,我可以通过将这些测试更改为以下内容来修复它们:

[TestMethod]
public void CanOnlyGetCurrentLinkedUsers()
{
    using (ShimsContext.Create())
    {
        System.Data.Entity.Fakes.ShimDbFunctions.TruncateTimeNullableOfDateTime =
            (DateTime? input) =>
            {
                return input.HasValue ? (DateTime?)input.Value.Date : null;
            };

        var up = new List<par_UserPlacement>
        {
            this.UserPlacementFactory(1, 2, 1), // Create a user placement that is current
            this.UserPlacementFactory(1, 3, 2, false) // Create a user placement that is not current
        }.AsQueryable();

        var set = DLTestHelper.GetMockSet<par_UserPlacement>(up);

        var context = DLTestHelper.Context;
        context.Setup(c => c.par_UserPlacement).Returns(set.Object);

        var getter = DLTestHelper.New<LinqUserGetLinkedUsersForParentUser>(context.Object);

        var output = getter.GetLinkedUsers(1);
    }

    var users = new List<User>();
    output.ProcessDataTable((DataRow row) => users.Add(new User(row)));

    Assert.AreEqual(1, users.Count);
    Assert.AreEqual(2, users[0].UserId);
}
[TestMethod]
public void CanolyGetCurrentLinkedUsers()
{
使用(ShimsContext.Create())
{
System.Data.Entity.Fakes.ShimDbFunctions.TruncateTimeNullableOfDateTime=
(日期时间?输入)=>
{
返回input.HasValue?(DateTime?)input.Value.Date:空;
};
var up=新列表
{
this.UserPlacementFactory(1,2,1),//创建当前的用户位置
this.UserPlacementFactory(1,3,2,false)//创建一个非当前的用户位置
}.AsQueryable();
var set=DLTestHelper.GetMockSet(up);
var context=DLTestHelper.context;
Setup(c=>c.par\u UserPlacement).Returns(set.Object);
var getter=DLTestHelper.New(context.Object);
var输出=getter.GetLinkedUsers(1);
}
var users=新列表();
output.ProcessDataTable((DataRow行)=>users.Add(新用户(行));
Assert.AreEqual(1,users.Count);
Assert.AreEqual(2,用户[0].UserId);
}

有一种方法可以做到这一点。由于业务逻辑的单元测试通常是鼓励的,并且由于业务逻辑对应用程序数据发出LINQ查询是完全可以的,那么对这些LINQ查询进行单元测试必须是完全可以的

不幸的是,Entity Framework的DbFunctions特性使我们无法对包含LINQ查询的代码进行单元测试。钼
var orderIdsByDate = (
    from o in repo.Orders
    group o by o.PlacedAt.Date 
         // here we used DateTime.Date 
         // and **NOT** DbFunctions.TruncateTime
    into g
    orderby g.Key
    select new { Date = g.Key, OrderIds = g.Select(x => x.Id) });
public class EntityRepository<T> : IQueryable<T> where T : class
{
    private readonly ObjectSet<T> _objectSet;
    private InterceptingQueryProvider _queryProvider = null;

    public EntityRepository<T>(ObjectSet<T> objectSet)
    {
        _objectSet = objectSet;
    }
    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        return _objectSet.AsEnumerable().GetEnumerator();
    }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _objectSet.AsEnumerable().GetEnumerator();
    }
    Type IQueryable.ElementType
    {
        get { return _objectSet.AsQueryable().ElementType; }
    }
    System.Linq.Expressions.Expression IQueryable.Expression
    {
        get { return _objectSet.AsQueryable().Expression; }
    }
    IQueryProvider IQueryable.Provider
    {
        get
        {
            if ( _queryProvider == null )
            {
                _queryProvider = new InterceptingQueryProvider(_objectSet.AsQueryable().Provider);
            }
            return _queryProvider;
        }
    }

    // . . . . . you may want to include Insert(), Update(), and Delete() methods
}
private class InterceptingQueryProvider : IQueryProvider
{
    private readonly IQueryProvider _actualQueryProvider;

    public InterceptingQueryProvider(IQueryProvider actualQueryProvider)
    {
        _actualQueryProvider = actualQueryProvider;
    }
    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        var specializedExpression = QueryExpressionSpecializer.Specialize(expression);
        return _actualQueryProvider.CreateQuery<TElement>(specializedExpression);
    }
    public IQueryable CreateQuery(Expression expression)
    {
        var specializedExpression = QueryExpressionSpecializer.Specialize(expression);
        return _actualQueryProvider.CreateQuery(specializedExpression);
    }
    public TResult Execute<TResult>(Expression expression)
    {
        return _actualQueryProvider.Execute<TResult>(expression);
    }
    public object Execute(Expression expression)
    {
        return _actualQueryProvider.Execute(expression);
    }
}
public static class QueryExpressionSpecializer
{
    private static readonly MethodInfo _s_dbFunctions_TruncateTime_NullableOfDateTime = 
        GetMethodInfo<Expression<Func<DateTime?, DateTime?>>>(d => DbFunctions.TruncateTime(d));

    private static readonly PropertyInfo _s_nullableOfDateTime_Value =
        GetPropertyInfo<Expression<Func<DateTime?, DateTime>>>(d => d.Value);

    public static Expression Specialize(Expression general)
    {
        var visitor = new SpecializingVisitor();
        return visitor.Visit(general);
    }
    private static MethodInfo GetMethodInfo<TLambda>(TLambda lambda) where TLambda : LambdaExpression
    {
        return ((MethodCallExpression)lambda.Body).Method;
    }
    public static PropertyInfo GetPropertyInfo<TLambda>(TLambda lambda) where TLambda : LambdaExpression
    {
        return (PropertyInfo)((MemberExpression)lambda.Body).Member;
    }

    private class SpecializingVisitor : ExpressionVisitor
    {
        protected override Expression VisitMember(MemberExpression node)
        {
            if ( node.Expression.Type == typeof(DateTime?) && node.Member.Name == "Date" )
            {
                return Expression.Call(_s_dbFunctions_TruncateTime_NullableOfDateTime, node.Expression);
            }

            if ( node.Expression.Type == typeof(DateTime) && node.Member.Name == "Date" )
            {
                return Expression.Property(
                    Expression.Call(
                        _s_dbFunctions_TruncateTime_NullableOfDateTime, 
                        Expression.Convert(
                            node.Expression, 
                            typeof(DateTime?)
                        )
                    ),
                    _s_nullableOfDateTime_Value
                );
            }

            return base.VisitMember(node);
        }
    }
}
public System.Data.DataTable GetLinkedUsers(int parentUserId, bool useDbFunctions = true)
{
    var today = DateTime.Now.Date;

    var queryable = from up in DB.par_UserPlacement
                where up.MentorId == mentorUserId;

    if (useDbFunctions) // use the DbFunctions
    {
     queryable = queryable.Where(up => 
     DbFunctions.TruncateTime(today) >= DbFunctions.TruncateTime(up.StartDate)
     && DbFunctions.TruncateTime(today) <= DbFunctions.TruncateTime(up.EndDate));
    }  
    else
    {
      // do db-functions equivalent here using C# logic
      // this is what the unit test path will invoke
      queryable = queryable.Where(up => up.StartDate < today);
    }                    

    var query = from up in queryable
                select new
                {
                    up.UserPlacementId,
                    up.Users.UserId,
                    up.Users.FirstName,
                    up.Users.LastName,
                    up.Placements.PlacementId,
                    up.Placements.PlacementName,
                    up.StartDate,
                    up.EndDate,
                };

    query = query.OrderBy(up => up.EndDate);

    return this.RunQueryToDataTable(query);
}
GetLinkedUsers(parentUserId: 10, useDbFunctions: false);
public static class CanTestDbFunctions
{
    [System.Data.Entity.DbFunction("Edm", "TruncateTime")]
    public static DateTime? TruncateTime(DateTime? dateValue)
    {
        ...
    }
}
[DbFunction("Edm", "TruncateTime")]
public static DateTime? TruncateTime(DateTime? dateValue)
{
    return dateValue?.Date;
}