web-dev-qa-db-fra.com

EntityFunctions.TruncateTime et tests unitaires

J'utilise la méthode System.Data.Objects.EntityFunctions.TruncateTime pour obtenir la date d'un élément datetime dans ma requête:

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

Cette méthode (je pense que la même chose s'applique aux autres méthodes EntityFunctions) ne peut pas être exécutée en dehors de LINQ to Entities. L'exécution de ce code dans un test unitaire, qui correspond effectivement à LINQ to objects, provoque l'envoi d'une NotSupportedException:

System.NotSupportedException: cette fonction ne peut être invoquée qu'à partir de LINQ auprès d'entités.

J'utilise un talon pour un référentiel avec une fausse DbSets dans mes tests.

Alors, comment dois-je tester l'unité de ma requête?

29
Jakub Konecki

Vous ne pouvez pas - si le test unitaire signifie que vous utilisez un faux référentiel en mémoire et que vous utilisez par conséquent LINQ to Objects. Si vous testez vos requêtes avec LINQ to Objects, vous ne testez pas votre application, mais uniquement votre faux référentiel.

Votre exception est le cas moins dangereux car il indique que vous avez un test rouge, mais probablement en fait une application qui fonctionne.

Le cas contraire est plus dangereux: avoir un test vert mais une application en panne ou des requêtes qui ne renvoient pas les mêmes résultats que votre test. Des requêtes comme ...

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

ou

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

... fonctionnera correctement dans votre test, mais pas avec LINQ to Entities dans votre application.

Une requête comme ...

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

... renverra potentiellement des résultats différents avec LINQ aux objets que LINQ aux entités.

Vous pouvez uniquement tester cela et la requête de votre question en construisant des tests d'intégration utilisant le fournisseur LINQ to Entities de votre application et une base de données réelle.

Modifier

Si vous voulez toujours écrire des tests unitaires, je pense que vous devez simuler la requête elle-même ou au moins des expressions dans la requête. Je peux imaginer que quelque chose dans le sens du code suivant pourrait fonctionner:

Créez une interface pour l'expression Where:

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

Créez une implémentation pour votre application ...

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

... et une deuxième implémentation dans votre projet de test unitaire:

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

Dans la classe où vous utilisez la requête, créez un membre privé de cette interface et deux constructeurs:

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

Utilisez le premier constructeur (par défaut) dans votre application:

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

Utilisez le second constructeur avec FakeEntityExpressions dans votre test unitaire:

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

Si vous utilisez un conteneur d'injection de dépendance, vous pouvez l'utiliser en injectant l'implémentation appropriée si IEntityExpressions dans le constructeur et si vous n'avez pas besoin du constructeur par défaut.

J'ai testé l'exemple de code ci-dessus et cela a fonctionné.

19
Slauma

Vous pouvez définir une nouvelle fonction statique (vous pouvez l’avoir comme méthode d’extension si vous le souhaitez):

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

Vous pouvez ensuite utiliser cette fonction dans LINQ to Entities et LINQ to Objects et cela fonctionnera. Cependant, cette méthode signifie que vous devrez remplacer les appels à EntityFunctions par des appels à votre nouvelle classe.

Une autre option, meilleure (mais plus complexe), consisterait à utiliser un visiteur d'expression et à écrire un fournisseur personnalisé pour vos DbSet en mémoire afin de remplacer les appels à EntityFunctions par des appels à des implémentations en mémoire.

15
Jean Hominal

Comme indiqué dans ma réponse à Comment tester unitaire GetNewValues ​​() contenant la fonction EntityFunctions.AddDays , vous pouvez utiliser un visiteur d'expression de requête pour remplacer les appels aux fonctions EntityFunctions par vos propres implémentations compatibles LINQ To Objects .

L'implémentation ressemblerait à:

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

Et voici un exemple d'utilisation avec 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]);
3
Jean Hominal

Bien que la réponse donnée par Smaula en utilisant la classe EntityExpressions me plaise, je pense que cela en fait un peu trop. Fondamentalement, il jette l'entité entière à la méthode, fait la comparaison et retourne un bool.

Dans mon cas, j'avais besoin de cette entité EntityFunctions.TruncateTime () pour créer un groupe, je n'avais donc pas de date à comparer ni de retour, mais je voulais simplement obtenir la bonne implémentation pour obtenir la partie date. Alors j'ai écrit:

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

Dans mon cas, je n'avais pas besoin de l'interface avec les deux implémentations séparées, mais cela devrait fonctionner de la même manière.

Je voulais partager cela, car il fait le plus petit possible. Il sélectionne uniquement la bonne méthode pour obtenir la partie de date.

2
Leyenda

Je me rends compte que c'est un vieux fil, mais je voulais quand même poster une réponse.

La solution suivante est faite en utilisant Shims

Je ne sais pas quelles versions (2013, 2012, 2010) ainsi que les combinaisons de saveurs (express, pro, premium, ultime) de Visual Studio vous permettent d'utiliser Shims, de sorte que cela pourrait ne pas être disponible pour tout le monde.

Voici le code affiché par l'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
}

Les éléments suivants se trouvent dans un projet de test unitaire et dans un fichier de test unitaire. Voici le test unitaire qui utiliserait 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);
     }
}

Je crois que c'est probablement la manière la plus propre et la plus non intrusive de tester le code qui utilise EntityFunctions sans générer cette exception NotSupportedException.

1
Igor

Vous pouvez également le vérifier de la manière suivante:

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