2012-03-06 28 views
29

che sto usando System.Data.Objects.EntityFunctions.TruncateTime metodo per ottenere la data parte di un datetime nella mia interrogazione:EntityFunctions.TruncateTime e unit test

if (searchOptions.Date.HasValue) 
    query = query.Where(c => 
     EntityFunctions.TruncateTime(c.Date) == searchOptions.Date); 

Questo metodo (Credo che lo stesso vale per altri metodi EntityFunctions) non possono essere eseguiti al di fuori di LINQ alle entità. L'esecuzione di questo codice in un test di unità, che è effettivamente LINQ agli oggetti, provoca un NotSupportedException da buttare:

System.NotSupportedException: Questa funzione può essere richiamato solo da LINQ alle entità.

Sto usando uno stub per un repository con il falso DbSets nei miei test.

Quindi, come devo testare la mia domanda?

+0

ho cancellato la mia risposta, non era utile per voi. In qualche modo sospettavo che non ti stavo dicendo niente di nuovo. Non ho idea di come gestire la tua query in un test unitario, a meno di non mettere un'intera query dietro un'interfaccia implementata LTO-friendly ('c => c.Date.Date == ...') nel tuo test unitario . – Slauma

+0

Potresti per favore annullare l'eliminazione della risposta? Penso che sia abbastanza valido e possa aiutare gli altri ... –

+0

Il metodo è solo un segnaposto. Quando il traduttore Linq to Entity elabora l'albero delle espressioni se vede questo metodo, sa come sostituirlo con un costrutto specifico del database. Pertanto, il metodo stesso non ha alcuna implementazione ma getta NotSupportedException. – Pawel

risposta

18

Non è possibile - se il test dell'unità significa che si sta utilizzando un repository falso in memoria e si sta quindi utilizzando LINQ to Objects. Se collaudi le tue query con LINQ to Objects, non hai testato la tua applicazione ma solo il tuo repository fasullo.

L'eccezione è il caso meno pericoloso in quanto indica che si dispone di un test rosso, ma probabilmente in realtà un'applicazione funzionante.

Più pericoloso è il caso al contrario: test verde ma un'applicazione o query che non restituiscono gli stessi risultati del test. Le query come ...

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

o

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

... funzioneranno bene nel test, ma non con LINQ to Entities nella vostra applicazione.

una query come ...

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

... sarà potenzialmente restituire risultati diversi con LINQ to Objects di LINQ to Entities.

È possibile testare solo questa e la query nella domanda creando test di integrazione che utilizzano il provider LINQ to Entities dell'applicazione e un database reale.

Modifica

Se avete ancora voglia di scrivere unit test Penso che tu debba falso query stessa o almeno espressioni nella query. Potevo immaginare che qualcosa sulla falsariga del seguente codice potrebbe funzionare:

creare un'interfaccia per la Where espressione:

public interface IEntityExpressions 
{ 
    Expression<Func<MyEntity, bool>> GetSearchByDateExpression(DateTime date); 
    // maybe more expressions which use EntityFunctions or SqlFunctions 
} 

Creare un'implementazione per l'applicazione ...

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

...e una seconda implementazione nel progetto di test Unità:

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

Nella classe in cui si utilizza la query creare un membro privato di questa interfaccia e due costruttori:

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

Utilizzare il primo (di default) costruttore nell'applicazione:

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 

utilizzare il secondo costruttore con FakeEntityExpressions nel vostro unit test:

Se si utilizza un contenitore di dipendenze per iniezione, è possibile sfruttarlo iniettando l'implementazione appropriata se IEntityExpressions nel costruttore e non è necessario il costruttore predefinito.

Ho testato il codice di esempio sopra e ha funzionato.

+3

Capisco la differenza tra L2O e L2E - So che i miei test non stanno replicando completamente il comportamento del server SQL ma posso ancora testare molti servizi. Sono abbastanza contento della possibilità di un risultato falso positivo - se succede, posso mettere a punto il test. Il vantaggio di avere il 99% di loro esaurisce i rischi. –

+1

Grazie per la modifica. Ero preoccupato che fosse necessario un tale trucco ;-) È un peccato che EF sia cotto a metà ... –

15

È possibile definire una nuova funzione statica (si può avere come un metodo di estensione, se volete):

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

Quindi è possibile quindi utilizzare tale funzione in LINQ to Entities e LINQ to Objects e sarà lavoro. Tuttavia, questo metodo implica la necessità di sostituire le chiamate su EntityFunctions con le chiamate alla nuova classe.

Un'altra opzione migliore (ma più implicata) sarebbe utilizzare un visitatore di espressioni e scrivere un provider personalizzato per i DbSet in-memory per sostituire le chiamate a EntityFunctions con chiamate a implementazioni in memoria.

+0

Ha funzionato per il mio caso ed è stata di gran lunga la soluzione più semplice! Grazie. –

+0

La migliore soluzione per un problema inizialmente complicato. – Anish

+0

Ancora meglio come metodo di estensione. public static DateTime? TruncateTime (date DateTime?) quindi utilizzare; myDate.TruncateTime() – tkerwood

3

Come indicato in my answer a How to Unit Test GetNewValues() which contains EntityFunctions.AddDays function, è possibile utilizzare un visitatore di espressione di query per sostituire le chiamate alle funzioni EntityFunctions con le proprie implementazioni compatibili LINQ To Objects.

L'implementazione sarà simile:

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

Ed ecco un esempio di utilizzo con 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

Anche se mi piace la risposta data da Smaula utilizzando la classe EntityExpressions, penso che fa un po 'troppo Fondamentalmente, getta l'intera entità sul metodo, confronta e restituisce un bool.

Nel mio caso, avevo bisogno di questo EntityFunctions.TruncateTime() per fare un gruppo, quindi non avevo nessuna data da confrontare, o bool da restituire, volevo solo ottenere l'implementazione corretta per ottenere la parte della data. Così ho scritto:

private static Expression<Func<DateTime?>> GetSupportedDatepartMethod(DateTime date, bool isLinqToEntities) 
    { 
     if (isLinqToEntities) 
     { 
      // Normal context 
      return() => EntityFunctions.TruncateTime(date); 
     } 
     else 
     { 
      // Test context 
      return() => date.Date; 
     } 
    } 

Nel mio caso, non ho avuto bisogno l'interfaccia con le due implementazioni separate, ma che dovrebbe funzionare lo stesso.

Ho voluto condividere questo, perché fa la più piccola cosa possibile. Seleziona solo il metodo giusto per ottenere la parte della data.

1

Mi rendo conto che questo è un thread vecchio ma volevo comunque postare una risposta.

La seguente soluzione è fatto usando Shims

io non sono sicuro di quello che le versioni (2013, 2012, 2010) e anche sapori (esprimere, pro, premium, Ultimate) combinazioni di Visual Studio consentono di utilizzare spessori così potrebbe essere che questo non è disponibile per tutti.

ecco il codice del PO ha registrato

// 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 
} 

Il seguente sarebbe situato a qualche progetto di unit test e alcuni file di test di unità. Ecco il test unitario che userebbe Shim.

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

Credo che questo è probabilmente il modo più pulito e più non intrusivo per testare il codice che fa uso di EntityFunctions senza generare quel NotSupportedException.

+0

Shim richiede Visual Studio Ultimate Version ref: https: //msdn.microsoft.com/en-us/library/hh549176.aspx – Rama

+1

@DRAM Dopo aver verificato gli shim sono disponibili nella versione Premium di VS 2013 (https://msdn.microsoft.com/en-us/library/hh549175.aspx), questo è quello che uso e ho fatto uso di Shim.In Visual Studio 2015 possono essere utilizzati nella versione Enterprise (utilizzata come premium e ultimate), dubito che siano disponibili nelle versioni pro o community ma non sono sicuri. – Igor

0

è anche possibile controllare nel seguente modo:

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