EntityFunctions.TruncateTime и модульные тесты
Я использую метод System.Data.Objects.EntityFunctions.TruncateTime
, чтобы получить дату в datetime в моем запросе:
if (searchOptions.Date.HasValue)
query = query.Where(c =>
EntityFunctions.TruncateTime(c.Date) == searchOptions.Date);
Этот метод (я считаю, что это относится и к другим методам EntityFunctions
) не может выполняться вне LINQ to Entities. Выполнение этого кода в unit test, который фактически является LINQ для объектов, вызывает выброс NotSupportedException
:
System.NotSupportedException: эту функцию можно вызывать только из LINQ to Entities.
Я использую заглушку для репозитория с поддельным DbSets
в моих тестах.
Итак, как я должен unit test мой запрос?
Ответы
Ответ 1
Вы не можете - если модульное тестирование означает, что вы используете поддельный репозиторий в памяти, и поэтому используете LINQ для объектов. Если вы тестируете свои запросы с помощью 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 to Entities.
Вы можете проверить это и запрос в своем вопросе, построив тесты интеграции, которые используют поставщик LINQ to Entities вашего приложения и реальную базу данных.
Edit
Если вы все еще хотите написать модульные тесты, я думаю, вы должны подделать сам запрос или, по крайней мере, выражения в запросе. Я мог представить, что что-то в соответствии с следующим кодом может работать:
Создайте интерфейс для выражения 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
}
}
... и вторая реализация в вашем проекте Unit test:
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 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
в unit test:
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
Если вы используете контейнер инъекции зависимостей, вы можете использовать его, введя соответствующую конструкцию if IEntityExpressions
в конструктор и не требуя конструктора по умолчанию.
Я тестировал приведенный выше пример кода и работал.
Ответ 2
Вы можете определить новую статическую функцию (вы можете использовать ее как метод расширения, если хотите):
[EdmFunction("Edm", "TruncateTime")]
public static DateTime? TruncateTime(DateTime? date)
{
return date.HasValue ? date.Value.Date : (DateTime?)null;
}
Затем вы можете использовать эту функцию в LINQ to Entities и LINQ to Objects, и она будет работать. Однако этот метод означает, что вам придется заменить вызовы на EntityFunctions
на вызовы нового класса.
Другим, лучшим (но более привлекательным) вариантом будет использование посетителя выражения и запись пользовательского поставщика для ваших DbSets в памяти, чтобы заменить вызовы на EntityFunctions
на вызовы в реализации в памяти.
Ответ 3
Как указано в моем ответе на Как Unit Test 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]);
Ответ 4
Хотя мне нравится ответ 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;
}
}
В моем случае мне не нужен интерфейс с двумя отдельными реализациями, но это должно работать точно так же.
Я хотел поделиться этим, потому что это делает все возможное. Он выбирает правильный метод для получения части даты.
Ответ 5
Я понимаю, что это старый поток, но все равно хотел опубликовать ответ.
Следующее решение выполняется с помощью Shims
Я не уверен, какие версии (2013, 2012, 2010), а также сочетания (express, pro, premium, ultimate) Visual Studio позволяют использовать Shims, поэтому может быть, что это не доступно для всех.
Вот код, опубликованный 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
}
В проекте unit test и в некотором unit test файле будет указано следующее. Вот unit test, который будет использовать 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.
Ответ 6
Вы также можете проверить его следующим образом:
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);