mock framework entity-framework unit-testing entity-framework-4 stub

entity framework - framework - EntityFunctions.TruncateTime y pruebas unitarias



mock dbcontext c# (6)

Aunque me gusta la respuesta dada por Smaula usando la clase EntityExpressions, creo que es demasiado. Básicamente, arroja toda la entidad al método, hace la comparación y devuelve un bool.

En mi caso, necesitaba este EntityFunctions.TruncateTime () para hacer un grupo, así que no tenía fecha para comparar, o bool para devolver, solo quería obtener la implementación correcta para obtener la parte de la fecha. Así que escribí:

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

En mi caso, no necesitaba la interfaz con las dos implementaciones separadas, pero eso debería funcionar igual.

Quería compartir esto, porque hace lo más pequeño posible. Solo selecciona el método correcto para obtener la parte de la fecha.

Estoy usando el método System.Data.Objects.EntityFunctions.TruncateTime para obtener la fecha de la fecha y hora en mi consulta:

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

Este método (creo que lo mismo se aplica a otros métodos de EntityFunctions ) no se puede ejecutar fuera de LINQ to Entities. La ejecución de este código en una prueba unitaria, que efectivamente es LINQ a objetos, hace que se NotSupportedException una NotSupportedException :

System.NotSupportedException: esta función solo puede invocarse desde LINQ a Entidades.

Estoy usando un código auxiliar para un repositorio con DbSets falsos en mis pruebas.

Entonces, ¿cómo debo probar mi consulta?


Como se indica en mi respuesta a Cómo probar la unidad GetNewValues ​​(), que contiene la función EntityFunctions.AddDays , puede usar un visitante de expresión de consulta para reemplazar las llamadas a EntityFunctions funciones EntityFunctions con sus propias implementaciones compatibles de LINQ To Objects.

La implementación se vería como:

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

Y aquí hay una muestra de uso 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]);


Me doy cuenta de que este es un hilo viejo, pero quería publicar una respuesta de todos modos.

La siguiente solución se realiza utilizando Shims

No estoy seguro de qué versiones (2013, 2012, 2010) y también las combinaciones de sabores (express, pro, premium, ultimate) de Visual Studio le permiten usar Shims, por lo que podría no estar disponible para todos.

Aquí está el código que el OP publicó

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

Lo siguiente se ubicaría en algún proyecto de prueba de unidad y algún archivo de prueba de unidad. Aquí está la prueba de la unidad que usaría 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); } }

Creo que esta es probablemente la forma más limpia y no intrusiva de probar el código que utiliza EntityFunctions sin generar esa excepción NotSupportedException.


No puede, si la prueba unitaria significa que está usando un repositorio falso en la memoria y, por lo tanto, está usando LINQ para objetos. Si prueba sus consultas con LINQ to Objects, no probó su aplicación, sino solo su repositorio falso.

Su excepción es el caso menos peligroso, ya que indica que tiene una prueba roja, pero probablemente sea una aplicación que funciona.

Más peligroso es el caso al revés: tener una prueba verde pero una aplicación o consultas que no devuelven los mismos resultados que su prueba. Consultas como ...

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

o

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

... funcionará bien en su prueba pero no con LINQ para Entidades en su aplicación.

Una consulta como ...

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

... potencialmente devolverá resultados diferentes con LINQ a objetos que LINQ a entidades.

Solo puede probar esto y la consulta en su pregunta construyendo pruebas de integración que utilizan el proveedor LINQ to Entities de su aplicación y una base de datos real.

Editar

Si aún desea escribir pruebas unitarias, creo que debe falsificar la consulta en sí misma o al menos las expresiones en la consulta. Podría imaginar que algo similar al código siguiente podría funcionar:

Crear una interfaz para la expresión Where :

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

Crea una implementación para tu aplicación ...

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

... y una segunda implementación en su proyecto de prueba de Unidad:

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

En su clase en la que está utilizando la consulta, cree un miembro privado de esta interfaz y dos constructores:

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

Use el primer constructor (predeterminado) en su aplicación:

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

Use el segundo constructor con FakeEntityExpressions en su prueba de unidad:

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 está utilizando un contenedor de inyección de dependencias, puede aprovechar al inyectar la implementación adecuada si IEntityExpressions en el constructor y no necesita el constructor predeterminado.

He probado el código de ejemplo anterior y funcionó.


Puede definir una nueva función estática (puede tenerla como método de extensión si lo desea):

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

Luego puedes usar esa función en LINQ to Entities y LINQ to Objects y funcionará. Sin embargo, ese método significa que tendría que reemplazar las llamadas a EntityFunctions con llamadas a su nueva clase.

Otra opción mejor (pero más complicada) sería utilizar un visitante de expresión y escribir un proveedor personalizado para sus DbSets en memoria para reemplazar las llamadas a EntityFunctions con llamadas a implementaciones en memoria.


También puedes comprobarlo de la siguiente manera:

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