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).
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.
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);
}
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.
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.
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;
}));
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.
- Passez un indicateur booléen 'useDbFunctions' à votre méthode avec la valeur par défaut true.
- 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.
- Lorsque vos tests unitaires appellent la méthode à tester, ils peuvent passer useDbFunctions: faux.
- 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.
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.