EntityFunctions.TruncateTime и юнит-тесты - PullRequest
29 голосов
/ 06 марта 2012

Я использую метод System.Data.Objects.EntityFunctions.TruncateTime для получения части даты datetime в моем запросе:

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

Этот метод (я считаю, что то же самое относится и к другим EntityFunctions методам) не может быть выполнен за пределамиLINQ to Entities.Выполнение этого кода в модульном тесте, который по сути является LINQ для объектов, приводит к выдаче NotSupportedException:

System.NotSupportedException: эта функция может быть вызвана только из LINQ to Entities.

В своих тестах я использую заглушку для хранилища с фальшивым DbSets.

Так как же мне выполнить юнит-тестирование моего запроса?

Ответы [ 6 ]

19 голосов
/ 06 марта 2012

Вы не можете - если модульное тестирование означает, что вы используете в памяти поддельный репозиторий и, следовательно, используете LINQ to Objects. Если вы тестировали свои запросы с помощью LINQ to Objects, вы тестировали не ваше приложение, а только свой поддельный репозиторий.

Ваше исключение - менее опасный случай, поскольку оно указывает на то, что у вас есть красный тест, но, вероятно, на самом деле работающее приложение.

С другой стороны, более опасно, если у вас есть зеленый тест, но вылетает приложение или запросы, которые не возвращают те же результаты, что и ваш тест. Запросы типа ...

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

или

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

... будет отлично работать в вашем тесте, но не с LINQ to Entities в вашем приложении.

запрос типа ...

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

... потенциально вернет результаты, отличные от LINQ для объектов, чем LINQ для объектов.

Вы можете проверить это и запрос в своем вопросе, только создав интеграционные тесты, использующие поставщика приложения LINQ to Entities и реальную базу данных.

Редактировать

Если вы все еще хотите написать модульные тесты, я думаю, что вы должны подделать сам запрос или хотя бы выражения в запросе. Я мог предположить, что что-то вроде следующего кода может работать:

Создать интерфейс для выражения Where:

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

Создание реализации для вашего приложения ...

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

... и вторая реализация в вашем модульном тестовом проекте:

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

В вашем классе, где вы используете запрос, создайте закрытый член этого интерфейса и два конструктора:

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

Используйте первый (по умолчанию) конструктор в вашем приложении:

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

Используйте второй конструктор с FakeEntityExpressions в своем модульном тесте:

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

Если вы используете контейнер для внедрения зависимостей, вы можете использовать его, вставив соответствующую реализацию, если IEntityExpressions, в конструктор и не нужен конструктор по умолчанию.

Я тестировал приведенный выше пример кода, и он работал.

15 голосов
/ 20 декабря 2012

Вы можете определить новую статическую функцию (вы можете использовать ее как метод расширения, если хотите):

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

Затем вы можете использовать эту функцию в LINQ to Entities и LINQ to Objects, и она будет работать. Однако этот метод означает, что вам придется заменить вызовы EntityFunctions вызовами вашего нового класса.

Другой, более лучший (но более сложный) вариант - использовать посетитель выражений и написать собственный провайдер для ваших наборов данных в памяти, чтобы заменить вызовы EntityFunctions вызовами реализаций в памяти.

3 голосов
/ 15 февраля 2013

Как указано в моем ответе на Как выполнить модульный тест GetNewValues ​​(), который содержит функцию EntityFunctions.AddDays , вы можете использовать посетитель выражения запроса для замены вызовов функций EntityFunctions с вашими собственными реализациями, совместимыми с LINQ To Objects.

Реализация будет выглядеть так:

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

А вот пример использования с 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]);
2 голосов
/ 15 апреля 2013

Хотя мне нравится ответ, данный Smaula с использованием класса EntityExpressions, я думаю, что он слишком много делает. По сути, он бросает всю сущность в метод, выполняет сравнение и возвращает логическое значение.

В моем случае мне понадобился этот EntityFunctions.TruncateTime () для создания группы, поэтому у меня не было даты для сравнения или bool для возврата, я просто хотел получить правильную реализацию для получения части даты. Итак, я написал:

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

В моем случае мне не нужен интерфейс с двумя отдельными реализациями, но он должен работать точно так же.

Я хотел бы поделиться этим, потому что это делает самую маленькую вещь возможную. Он только выбирает правильный метод для получения части даты.

1 голос
/ 21 апреля 2015

Я понимаю, что это старая тема, но все равно хотел опубликовать ответ.

Следующее решение сделано с использованием Шимс

Я не уверен, какие версии (2013, 2012, 2010), а также варианты (Express, Pro, Premium, Ultimate) комбинаций Visual Studio позволяют использовать Shims, поэтому может оказаться, что это доступно не всем.

Вот код, опубликованный ОП

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

Следующая информация будет размещена в каком-либо проекте модульного тестирования и в некотором файле модульного теста. Вот модульный тест, который будет использовать 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);
     }
}

Я полагаю, что это, вероятно, самый чистый и самый ненавязчивый способ тестирования кода, который использует EntityFunctions без генерации этого NotSupportedException.

0 голосов
/ 27 июня 2017

Вы также можете проверить это следующим образом:

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