如何在unit testing中使用Moq和DbFunction来防止NotSupportedException?

我目前正在尝试对通过entity framework运行的查询运行一些unit testing。 查询本身在实时版本上运行没有任何问题,但unit testing总是失败。

我把它缩小到我对DbFunctions.TruncateTime的使用,但我不知道如何通过这种方式来获取unit testing来反映实时服务器上发生的情况。

这是我正在使用的方法:

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)  up.EndDate); return this.RunQueryToDataTable(query); } 

如果我注释掉带有DbFunctions的行,则测试全部通过(除了检查只运行给定日期的有效结果的那些)。

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

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

  [TestMethod] public void CanOnlyGetCurrentLinkedUsers() { var up = new List { 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(up); var context = DLTestHelper.Context; context.Setup(c => c.par_UserPlacement).Returns(set.Object); var getter = DLTestHelper.New(context.Object); var output = getter.GetLinkedUsers(1); var users = new List(); output.ProcessDataTable((DataRow row) => students.Add(new UserStudent(row))); Assert.AreEqual(1, users.Count); Assert.AreEqual(2, users[0].UserId); } 

编辑2:这是来自相关测试的消息和调试跟踪:

 Test Result: Failed Message: Assert.AreEqual failed. Expected:. Actual: Debug Trace: This function can only be invoked from LINQ to Entities 

从我读过的,这是因为没有这个方法的LINQ to Entities实现可以在这个地方用于unit testing,尽管有实时版本(因为它正在查询SQL服务器)。

我知道我已经迟到了,但一个非常简单的解决方法就是编写自己的方法,该方法使用DbFunction属性。 然后使用该函数而不是DbFunctions.TruncateTime。

 [DbFunction("Edm", "TruncateTime")] public static DateTime? TruncateTime(DateTime? dateValue) { return dateValue?.Date; } 

使用此函数将在Linq to Entities使用时执行EDM TruncateTime方法,否则将运行提供的代码。

感谢所有人的帮助,我在阅读了qujck提到的垫片后,设法找到了一个对我有用的解决方案。 添加一个假的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 { 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(up); var context = DLTestHelper.Context; context.Setup(c => c.par_UserPlacement).Returns(set.Object); var getter = DLTestHelper.New(context.Object); var output = getter.GetLinkedUsers(1); } var users = new List(); output.ProcessDataTable((DataRow row) => users.Add(new User(row))); Assert.AreEqual(1, users.Count); Assert.AreEqual(2, users[0].UserId); } 

看看这个答案: https : //stackoverflow.com/a/14975425/1509728

老实说,在考虑它之后,我完全同意答案,并且通常遵循我的EF查询针对数据库进行测试的原则,并且仅使用Moq测试我的应用程序代码。

看起来没有优雅的解决方案使用Moq来测试上面的查询的EF查询,而有一些hacky的想法。 例如这一个以及随后的答案。 两者似乎都可以为你效劳。

另一种测试查询的方法是在我工作的另一个项目上实现的方法:使用VS开箱unit testing,每个查询(再次重构为自己的方法)测试将包装在事务范围内。 然后项目的测试框架将负责将虚假数据手动输入到数据库中,并且查询将尝试过滤此虚假数据。 最后,事务永远不会完成,因此会回滚。 由于事务范围的性质,这可能不是许多项目的理想方案。 很可能不是在prod环境中。

否则,如果必须继续模拟function,则可能需要考虑其他模拟框架。

有办法做到这一点。 由于通常鼓励 对业务逻辑进行unit testing ,并且由于业务逻辑完全可以 针对应用程序数据发出LINQ查询 ,因此这些LINQ查询进行unit testing必须完全正常

不幸的是, entity framework的 DbFunctions特性使我们无法对包含LINQ查询的unit testing代码进行unit testing。 此外,在业务逻辑中使用DbFunction在架构上是错误的,因为它将业务逻辑层与特定的持久性技术(这是一个单独的讨论)相结合。

话虽如此, 我们的目标是能够像这样运行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) }); 

unit testing中 ,这将归结为针对预先安排的普通实体arrays运行的LINQ到对象 (例如)。 在实际运行中,它必须针对entity framework的真实ObjectContext

这是实现它的一个方法 – 虽然,它需要你的几个步骤。 我正在削减一个真实的例子:

步骤1.在我们自己的IQueryable实现中包装ObjectSet ,以便提供我们自己的IQueryProvider拦截包装器。

 public class EntityRepository : IQueryable where T : class { private readonly ObjectSet _objectSet; private InterceptingQueryProvider _queryProvider = null; public EntityRepository(ObjectSet objectSet) { _objectSet = objectSet; } IEnumerator IEnumerable.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 } 

第2步 。 实现拦截查询提供程序,在我的示例中,它是EntityRepository的嵌套类:

 private class InterceptingQueryProvider : IQueryProvider { private readonly IQueryProvider _actualQueryProvider; public InterceptingQueryProvider(IQueryProvider actualQueryProvider) { _actualQueryProvider = actualQueryProvider; } public IQueryable CreateQuery(Expression expression) { var specializedExpression = QueryExpressionSpecializer.Specialize(expression); return _actualQueryProvider.CreateQuery(specializedExpression); } public IQueryable CreateQuery(Expression expression) { var specializedExpression = QueryExpressionSpecializer.Specialize(expression); return _actualQueryProvider.CreateQuery(specializedExpression); } public TResult Execute(Expression expression) { return _actualQueryProvider.Execute(expression); } public object Execute(Expression expression) { return _actualQueryProvider.Execute(expression); } } 

步骤3.最后,实现一个名为QueryExpressionSpecializer的辅助类,它将使用DbFunctions.TruncateTime替换DateTime.Date

 public static class QueryExpressionSpecializer { private static readonly MethodInfo _s_dbFunctions_TruncateTime_NullableOfDateTime = GetMethodInfo>>(d => DbFunctions.TruncateTime(d)); private static readonly PropertyInfo _s_nullableOfDateTime_Value = GetPropertyInfo>>(d => d.Value); public static Expression Specialize(Expression general) { var visitor = new SpecializingVisitor(); return visitor.Visit(general); } private static MethodInfo GetMethodInfo(TLambda lambda) where TLambda : LambdaExpression { return ((MethodCallExpression)lambda.Body).Method; } public static PropertyInfo GetPropertyInfo(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); } } } 

当然, QueryExpressionSpecializer的上述实现可以通用化,允许插入任意数量的额外转换,允许自定义类型的成员在LINQ查询中使用,即使它们不为Entity Framework所知。

嗯,不确定,但你不能做这样的事情吗?

 context.Setup(s => DbFunctions.TruncateTime(It.IsAny())) .Returns(new Func( (x) => { /* whatever modification is required here */ return x; //or return modified; })); 

因为我最近遇到了同样的问题,并选择了一个更简单的解决方案,想在这里发布..这个解决方案不需要垫片,模拟,没有任何扩展等。

  1. 将’useDbFunctions’布尔标志传递给您的方法,默认值为true。
  2. 当您的实时代码执行时,您的查询将使用DbFunctions,一切都会正常工作。 由于默认值,呼叫者无需担心它。
  3. 当您的unit testing调用方法进行测试时,它们可以传递useDbFunctions:false。
  4. 在您的方法中,您可以使用该标志来组合您的IQueryable ..如果useDbFunctions为true,则使用DbFunctions将谓词添加到可查询对象。 如果useDbFunctions为false,则跳过DbFunctions方法调用,并执行显式的C#等效解决方案。

这样,您的unit testing将检查几乎95%的方法与实时代码相同。 你仍然有“DbFunctions”的delta与你的等效代码相比,但要勤奋,95%看起来会有很大的收益。

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

unit testing将调用方法:

 GetLinkedUsers(parentUserId: 10, useDbFunctions: false); 

因为unit testing会设置本地DbContext实体,所以C#logic / DateTime函数可以工作。

Mocks的使用在不久前结束了。 不要模拟,只需连接到真正的数据库。 在测试开始时重新生成/种子DB。 如果你仍想继续使用模拟,那么创建自己的方法,如下所示。 IT改变了行为运行时。 使用真实数据库时,它使用数据库函数,否则使用此方法。 使用此方法替换代码中的DBfunctions方法

 public static class CanTestDbFunctions { [System.Data.Entity.DbFunction("Edm", "TruncateTime")] public static DateTime? TruncateTime(DateTime? dateValue) { ... } } 

这是被调用的真正函数。 请记住,时间不能从DateTime对象中删除,与午夜一起生活或创建等效的字符串。