2012-03-06 123 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);