2012-03-06 6 views
29

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

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

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

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

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

Итак, как мне выполнить проверку моего запроса?

+0

Я удалил свой ответ, это не было полезно для вас. Почему-то я подозревал, что не говорю вам ничего нового. Я понятия не имею, как обращаться с вашим запросом в модульном тесте, если не поставить весь запрос за интерфейс, который реализован в соответствии с LTO ('c => c.Date.Date == ...') в вашем модульном тесте , – Slauma

+0

Не могли бы вы восстановить свой ответ? Я думаю, что это вполне справедливо и может помочь другим ... –

+0

Метод - это только владелец места. Когда переводчик Linq to Entity обрабатывает дерево выражений, если видит этот метод, он знает, как заменить его конструкцией, специфичной для базы данных. Поэтому сам метод не имеет никакой реализации, но генерирует NotSupportedException. – Pawel

ответ

18

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

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

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

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

или

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

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

Запрос как ...

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

... потенциально возвращать разные результаты с помощью LINQ к объектам, чем LINQ к Entities.

Вы можете проверить это и запрос в своем вопросе, построив интеграционные тесты, которые используют поставщик 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 в конструктор и не нужен конструктор по умолчанию.

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

+3

Я понимаю разницу между L2O и L2E - я знаю, что мои тесты не полностью реплицируют поведение SQL-сервера, но я все же могу проверить множество сервисов. Я вполне доволен возможностью ложного положительного результата - если это произойдет, я смогу точно настроить тест. Преимущество того, чтобы 99% из них работали, снижает риски. –

+1

Благодарим вас за редактирование. Я был обеспокоен тем, что нужен такой взлом ;-) Жаль, что EF наполовину испечен ... –

15

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

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

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

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

+0

Он работал на мой случай и был самым простым решением! Благодарю. –

+0

Лучшее решение изначально сложной проблемы. – Anish

+0

Еще лучше, как метод расширения. public static DateTime? TruncateTime (дата DateTime?) затем использовать; myDate.TruncateTime() – tkerwood

3

Как указано в: my answer - How to Unit Test GetNewValues() which contains EntityFunctions.AddDays function, вы можете использовать выражение выражения запроса для замены вызовов на функции 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

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

В моем случае мне понадобился этот 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

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

следующее решение осуществляется с помощью Shims

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

Вот код ОП отправил

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

Для прошивки требуется версия Visual Studio Ultimate ref: https: //msdn.microsoft.com/en-us/library/hh549176.aspx – Rama

+1

@DRAM После проверки прокладок доступны в версии Premium версии 2013 (https://msdn.microsoft.com/en-us/library/hh549175.aspx), это то, что я использую, и я использовал Shims.В Visual Studio 2015 они могут использоваться в версии Enterprise (обычно это премиум-класс), я сомневаюсь, что они доступны в версиях pro или community, но не уверены. – Igor

0

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

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); 
Смежные вопросы