Comment utiliser Moq et DbFunctions dans les tests unitaires pour empêcher une exception NotSupportedException?

J'essaie actuellement d'exécuter des tests unitaires sur une requête qui s'exécute via Entity Framework. La requête elle-même s'exécute sans aucun problème sur la version live, mais les tests unitaires échouent toujours.

J'ai réduit cela à mon utilisation de DbFunctions.TruncateTime, mais je ne connais pas de moyen de contourner cela pour que les tests unitaires reflètent ce qui se passe sur le serveur en direct.

Voici la méthode que j'utilise:

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

Si je commente les lignes avec DbFunctions in, Les tests passent tous (à l'exception de ceux qui vérifient que seuls les résultats valides pour une date donnée sont exécutés).

Existe-t-il un moyen de fournir une version moquée de DbFunctions.TruncateTime à utiliser dans ces tests? Essentiellement, il devrait juste renvoyer Datetime.Date, mais ce n'est pas disponible dans les requêtes EF.

Edit: Voici le test qui échoue qui utilise la vérification de la date:

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

Edit 2: c'est le message et la trace de débogage du test en question:

Test Result: Failed

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

Debug Trace: This function can only be invoked from LINQ to Entities

D'après ce que j'ai lu, c'est parce qu'il n'y a pas D'implémentation LINQ to Entities de cette méthode qui pourrait être utilisée à cet endroit pour le test unitaire, bien qu'il y en ait sur la version live (car il interroge un serveur SQL).

21
demandé sur JohnOsborne 2014-01-28 18:06:28

7 réponses

Je sais que je suis en retard au jeu, mais une solution très simple est d'écrire votre propre méthode qui utilise L'attribut DbFunction. Ensuite, utilisez cette fonction au lieu de DbFunctions.TruncateTime.

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

L'utilisation de cette fonction exécutera la méthode EDM TruncateTime lorsqu'elle est utilisée par Linq aux entités et exécutera le code fourni autrement.

17
répondu esteuart 2017-08-07 17:16:05

Merci pour toute l'aide de tout le monde, j'ai réussi à trouver une solution qui a fonctionné pour moi après avoir lu sur les cales mentionnées par qujck. Après avoir ajouté un faux assemblage de EntityFramework, j'ai pu corriger ces tests en les changeant comme suit:

[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);
}
15
répondu Lyise 2014-01-31 09:15:40

Découvrez cette réponse: https://stackoverflow.com/a/14975425/1509728

Pour être honnête, en y réfléchissant, je suis totalement d'accord avec la réponse et je respecte généralement le principe selon lequel mes requêtes EF sont testées par rapport à la base de données et que seul mon code d'application est testé avec Moq.

Il semble qu'il n'y ait pas de solution élégante à l'utilisation de Moq pour tester les requêtes EF avec votre requête ci-dessus, alors qu'il y a quelques idées hacky là-bas. Par exemple celui-ci et la réponse qui suit. Les deux semblent pouvoir travailler pour vous.

Une autre approche pour tester vos requêtes serait implémentée sur un autre projet sur lequel j'ai travaillé: en utilisant VS out of box unit tests, chaque requête (encore une fois refactorisée dans sa propre méthode) test serait enveloppé dans une portée de transaction. Ensuite, le framework de test du projet prendrait soin d'entrer manuellement des données fausses dans la base de données et la requête essaierait de filtrer ces données fausses. À la fin, la transaction n'est jamais terminé donc il est annulé. en raison de la nature des étendues de transaction, cela pourrait ne pas être un scénario idéal pour beaucoup de projets. très probablement pas sur les environnements prod.

Sinon, si vous devez continuer à vous moquer des fonctionnalités, vous pouvez envisager d'autres frameworks moqueurs.

1
répondu saml 2017-05-23 11:53:55

Il y a un moyen de le faire. Puisque les tests unitaires de la logique métiersont généralement encouragés, et comme il est parfaitement correctpour que la logique métier émette des requêtes LINQ contre des données d'application, alors il doit être parfaitement correct de tester ces requêtes LINQ.

Malheureusement, DbFunctions fonctionnalité de Entity Framework tue notre capacité à tester le code unitaire qui contient des requêtes LINQ. En outre, il est architecturalement incorrect d'utiliser DbFunctions en logique métier, car il Couple la couche de logique métier à une technologie de persistance spécifique (qui est une discussion séparée).

Cela dit, notre objectif est la possibilité de exécuter LINQ query comme ceci:

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

Dans unit test , cela se résoudra à LINQ-to-Objects s'exécutant sur un tableau simple d'entités disposées à l'avance (par exemple). Dans une vraie course, ça doit marcher contre un Réel ObjectContext cadre D'entité.

Voici une recette pour y parvenir - bien que cela nécessite quelques étapes. Je coupe un vrai exemple de travail:

Étape 1. Wrap ObjectSet<T> dans notre propre implémentation de IQueryable<T> afin de fournir notre propre wrapper d'interception de IQueryProvider.

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
}

Étape 2. Implémentez le fournisseur de requête d'interception, dans mon exemple c'est une classe imbriquée à l'intérieur EntityRepository<T>:

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

Étape 3. enfin, implémentez une classe d'aide nommée QueryExpressionSpecializer, qui remplacerait DateTime.Date par DbFunctions.TruncateTime.

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

Bien sûr, l'implémentation ci-dessus de QueryExpressionSpecializer peut être généralisée pour permettre de brancher n'importe quel nombre de conversions supplémentaires, permettant aux membres de types personnalisés d'être utilisés dans les requêtes LINQ, même s'ils ne sont pas connus de Entity Framework.

1
répondu felix-b 2015-02-09 05:48:45

Hmm, pas sûr, mais ne pourriez-vous pas faire quelque chose comme ça?

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;
        }));
0
répondu saml 2014-01-28 17:44:52

Depuis que j'ai frappé le même problème récemment, et j'ai opté pour une solution plus simple, je voulais le poster ici.. cette solution ne nécessite pas de cales, de moqueries, rien d'expansif, etc.

  1. Passez un indicateur booléen 'useDbFunctions' à votre méthode avec la valeur par défaut true.
  2. lorsque votre code live s'exécute, votre requête utilisera DbFunctions et tout fonctionnera. En raison de la valeur par défaut, les appelants ne doivent pas s'inquiéter à ce sujet.
  3. Lorsque vos tests unitaires appellent la méthode à tester, ils peuvent passer useDbFunctions: faux.
  4. dans votre méthode, vous pouvez utiliser le drapeau pour composer votre IQueryable.. si useDbFunctions vaut true, utilisez DbFunctions pour ajouter le prédicat à la requête. si useDbFunctions a la valeur false, ignorez L'appel de méthode DbFunctions et effectuez une solution équivalente C# explicite.

De cette façon, vos tests unitaires vérifieront presque 95% de votre méthode en parité avec le code live. Vous avez toujours le delta de "DbFunctions" par rapport à votre code équivalent, mais soyez diligent il et le 95% ressemblera à beaucoup de gain.

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

Les tests unitaires invoqueront le mthod comme suit:

GetLinkedUsers(parentUserId: 10, useDbFunctions: false);

Parce que les tests unitaires auraient configuré des entités DbContext locales, les fonctions C # logic / DateTime fonctionneraient.

-1
répondu Raja Nadar 2015-10-07 19:39:17

L'utilisation de Mocks a pris fin il y a quelque temps. Ne vous moquez pas, connectez-vous simplement à la base de données réelle. Régénérer / Seed DB au début de l'essai. Si vous voulez toujours aller de l'avant avec des mocks, créez votre propre méthode comme indiqué ci-dessous. IL modifie le comportement de l'exécution. Lorsque vous utilisez Real DB, il utilise des fonctions DB, sinon cette méthode. Remplacez la méthode DBfunctions dans le code par cette méthode

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

C'est la véritable fonction qui est appelée. Et rappelez-vous, l'heure ne peut pas être supprimée de L'objet DateTime, live with midnight ou créez une chaîne équivalente.

-6
répondu Blue Clouds 2016-03-08 14:16:50