EntityFunctions.TruncateTime y pruebas unitarias


Estoy usando el método System.Data.Objects.EntityFunctions.TruncateTime para obtener la parte de fecha de una datetime 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 EntityFunctions) no se puede ejecutar fuera de LINQ to Entities. Ejecutar este código en una prueba unitaria, que efectivamente es LINQ para objetos, causa que se lance un NotSupportedException:

Sistema.NotSupportedException: Esta función solo se puede invocar desde LINQ to entities.

Estoy usando un stub para un repositorio con fake DbSets en mi prueba.

Entonces, ¿cómo debo probar unitariamente mi consulta?

Author: Jakub Konecki, 2012-03-06

6 answers

No puede - si la prueba unitaria significa que está utilizando un repositorio falso en memoria y, por lo tanto, está utilizando LINQ to Objects. 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 en realidad una aplicación que funciona.

Más peligroso es el caso al revés: Tener una prueba verde pero una aplicación que se bloquea 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 to entities en su aplicación.

, Una consulta como...

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

...potencialmente devolverá resultados diferentes con LINQ to Objects que LINQ to Entities.

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

Editar

Si todavía desea escribir pruebas unitarias, creo que debe falsificar la consulta en sí o al menos las expresiones en la consulta. Me imagino que algo parecido al siguiente código podría funcionar:

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

Cree una implementación para su 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 prueba unitaria proyecto:

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

Utilice el segundo constructor con FakeEntityExpressions en su prueba unitaria:

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, podría aprovecharlo inyectando la implementación adecuada if IEntityExpressions en el constructor y no necesita el valor predeterminado constructor.

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

 19
Author: Slauma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-07 11:35:45

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

Entonces puede 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 involucrada) sería usar 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.

 15
Author: Jean Hominal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-20 18:46:48

Como se describe en mi respuesta a Cómo probar Unitariamente GetNewValues() que contiene EntityFunctions.AddDays function , puede usar una expresión de consulta visitor para reemplazar las llamadas a funciones EntityFunctions con sus propias implementaciones compatibles con LINQ To Objects.

La implementación se vería así:

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]);
 3
Author: Jean Hominal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:29:52

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

En mi caso, necesitaba esta EntityFunctions.TruncateTime () para hacer un grupo, así que no tenía fecha para comparar, o bool para volver, solo quería obtener la implementación correcta para obtener la parte de 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 los 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 fecha.

 2
Author: Leyenda,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-15 12:21:41

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 usando Cuñas

No estoy seguro de qué versiones (2013, 2012, 2010) y también sabores (express, pro, premium, ultimate) combinaciones de Visual Studio le permiten utilizar Shims por lo que podría ser que esto no está disponible para todo el mundo.

Aquí está el código publicado por la 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
}

Lo siguiente se ubicaría en algún proyecto de prueba unitaria y alguna prueba unitaria file. Aquí está la prueba unitaria que usaría Cuñas.

// 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 código que hace uso de EntityFunctions sin generar esa NotSupportedException.

 1
Author: Igor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-21 15:03:05

También puede 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);
 0
Author: Илья Красный,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-28 14:42:08