Как использовать Moq и DbFunctions в модульных тестах, чтобы предотвратить исключение NotSupportedException?

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

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

Вот метод, который я использую:

    public System.Data.DataTable GetLinkedUsers(int parentUserId)
    {
        var today = DateTime.Now.Date;

        var query = from up in DB.par_UserPlacement
                    where up.MentorId == mentorUserId
                        && DbFunctions.TruncateTime(today) >= DbFunctions.TruncateTime(up.StartDate)
                        && DbFunctions.TruncateTime(today) <= DbFunctions.TruncateTime(up.EndDate)
                    select new
                    {
                        up.UserPlacementId,
                        up.Users.UserId,
                        up.Users.FirstName,
                        up.Users.LastName,
                        up.Placements.PlacementId,
                        up.Placements.PlacementName,
                        up.StartDate,
                        up.EndDate,
                    };

        query = query.OrderBy(up => up.EndDate);

        return this.RunQueryToDataTable(query);
    }

Если я прокомментирую строки с DbFunctions in, все тесты проходят (за исключением тех, которые проверяют, что выполняются только действительные результаты для заданной даты).

Есть ли способ, которым я могу предоставить издеваемую версию DbFunctions.TruncateTime для использования в этих тестах? По сути, он должен просто возвращать Datetime.Date, но это недоступно в запросах EF.

Изменить: В этом тесте, который не работает, использует проверку даты:

    [TestMethod]
    public void CanOnlyGetCurrentLinkedUsers()
    {
        var up = new List<par_UserPlacement>
        {
            this.UserPlacementFactory(1, 2, 1), // Create a user placement that is current
            this.UserPlacementFactory(1, 3, 2, false) // Create a user placement that is not current
        }.AsQueryable();

        var set = DLTestHelper.GetMockSet<par_UserPlacement>(up);

        var context = DLTestHelper.Context;
        context.Setup(c => c.par_UserPlacement).Returns(set.Object);

        var getter = DLTestHelper.New<LinqUserGetLinkedUsersForParentUser>(context.Object);

        var output = getter.GetLinkedUsers(1);

        var users = new List<User>();
        output.ProcessDataTable((DataRow row) => students.Add(new UserStudent(row)));

        Assert.AreEqual(1, users.Count);
        Assert.AreEqual(2, users[0].UserId);
    }

Изменить 2: Это трассировка сообщения и отладки из рассматриваемого теста:

Test Result: Failed

Message: Assert.AreEqual failed. Expected:<1>. Actual:<0>

Debug Trace: This function can only be invoked from LINQ to Entities

Из того, что я прочитал, это связано с тем, что в этом месте не существует реализации LINQ to Entities этого метода, который можно было бы использовать в этом месте для Unit Test, хотя есть в живой версии (поскольку он запрашивает SQL-сервер).

Ответы

Ответ 1

Спасибо за помощь всем, мне удалось найти решение, которое работало для меня после прочтения прокладок, упомянутых в qujck. После добавления поддельной сборки EntityFramework я смог исправить эти тесты, изменив их на следующее:

[TestMethod]
public void CanOnlyGetCurrentLinkedUsers()
{
    using (ShimsContext.Create())
    {
        System.Data.Entity.Fakes.ShimDbFunctions.TruncateTimeNullableOfDateTime =
            (DateTime? input) =>
            {
                return input.HasValue ? (DateTime?)input.Value.Date : null;
            };

        var up = new List<par_UserPlacement>
        {
            this.UserPlacementFactory(1, 2, 1), // Create a user placement that is current
            this.UserPlacementFactory(1, 3, 2, false) // Create a user placement that is not current
        }.AsQueryable();

        var set = DLTestHelper.GetMockSet<par_UserPlacement>(up);

        var context = DLTestHelper.Context;
        context.Setup(c => c.par_UserPlacement).Returns(set.Object);

        var getter = DLTestHelper.New<LinqUserGetLinkedUsersForParentUser>(context.Object);

        var output = getter.GetLinkedUsers(1);
    }

    var users = new List<User>();
    output.ProcessDataTable((DataRow row) => users.Add(new User(row)));

    Assert.AreEqual(1, users.Count);
    Assert.AreEqual(2, users[0].UserId);
}

Ответ 2

Я знаю, что я опаздываю на игру, но очень простое решение - написать свой собственный метод, который использует атрибут DbFunction. Затем используйте эту функцию вместо DbFunctions.TruncateTime.

[DbFunction("Edm", "TruncateTime")]
public static DateTime? TruncateTime(DateTime? dateValue)
{
    return dateValue?.Date;
}

Использование этой функции будет выполнять метод EDM TruncateTime при использовании Linq to Entities и в противном случае будет выполнять предоставленный код.

Ответ 3

Проверьте этот ответ: fooobar.com/questions/292494/...

Честно говоря, размышляя об этом, я полностью согласен с ответом и в целом придерживаюсь принципа, согласно которому мои запросы EF проверяются по базе данных, и только мой код приложения проверяется с помощью Moq.

Похоже, нет элегантного решения для использования Moq для тестирования запросов EF с вашим запросом выше, в то время как есть некоторые хакерские идеи. Например этот и ответ, который следует за ним. Оба выглядят так, как будто они могут работать на вас.

Еще один подход к тестированию ваших запросов будет реализован в другом проекте, в котором я работал: с использованием VS вне коробки модульных тестов каждый запрос (снова реорганизованный в свой собственный метод) будет завершен в область транзакций. Тогда структура тестового проекта будет заботиться о том, чтобы вручную вводить фальшивые данные в db, и запрос попытается отфильтровать эти фальшивые данные. В конце транзакция никогда не завершается, поэтому она откатывается назад. В связи с природой областей транзакций это может быть не идеальный сценарий для большого количества проектов. Скорее всего, не в среде prod.

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

Ответ 4

Есть способ сделать это. Поскольку единичное тестирование бизнес-логики обычно рекомендуется, а так как оно отлично ОК для бизнес-логики для выпуска запросов LINQ к данным приложения, то он должен быть отлично до unit test тех запросов LINQ.

К сожалению, функция DbFunctions Entity Framework убивает нашу способность unit test код, содержащий запросы LINQ. Кроме того, архитектурно неправильно использовать DbFunctions в бизнес-логике, поскольку он соединяет уровень бизнес-логики с определенной технологией непрерывности (что является отдельным обсуждением).

Сказав, что наша цель - это возможность выполнить запрос LINQ следующим образом:

var orderIdsByDate = (
    from o in repo.Orders
    group o by o.PlacedAt.Date 
         // here we used DateTime.Date 
         // and **NOT** DbFunctions.TruncateTime
    into g
    orderby g.Key
    select new { Date = g.Key, OrderIds = g.Select(x => x.Id) });

В unit test это будет сжиматься до LINQ-to-Objects, работающего против простого массива сущностей, расположенных заранее (например). В реальном прогоне он должен работать против реального ObjectContext Entity Framework.

Вот рецепт достижения - хотя это требует нескольких шагов. Я сокращаю настоящий рабочий пример:

Шаг 1. Оберните ObjectSet<T> внутри нашей собственной реализации IQueryable<T>, чтобы предоставить нашу собственную перехватывающую оболочку IQueryProvider.

public class EntityRepository<T> : IQueryable<T> where T : class
{
    private readonly ObjectSet<T> _objectSet;
    private InterceptingQueryProvider _queryProvider = null;

    public EntityRepository<T>(ObjectSet<T> objectSet)
    {
        _objectSet = objectSet;
    }
    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        return _objectSet.AsEnumerable().GetEnumerator();
    }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _objectSet.AsEnumerable().GetEnumerator();
    }
    Type IQueryable.ElementType
    {
        get { return _objectSet.AsQueryable().ElementType; }
    }
    System.Linq.Expressions.Expression IQueryable.Expression
    {
        get { return _objectSet.AsQueryable().Expression; }
    }
    IQueryProvider IQueryable.Provider
    {
        get
        {
            if ( _queryProvider == null )
            {
                _queryProvider = new InterceptingQueryProvider(_objectSet.AsQueryable().Provider);
            }
            return _queryProvider;
        }
    }

    // . . . . . you may want to include Insert(), Update(), and Delete() methods
}

Шаг 2. Реализуйте провайдер запросов перехвата, в моем примере это вложенный класс внутри EntityRepository<T>:

private class InterceptingQueryProvider : IQueryProvider
{
    private readonly IQueryProvider _actualQueryProvider;

    public InterceptingQueryProvider(IQueryProvider actualQueryProvider)
    {
        _actualQueryProvider = actualQueryProvider;
    }
    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        var specializedExpression = QueryExpressionSpecializer.Specialize(expression);
        return _actualQueryProvider.CreateQuery<TElement>(specializedExpression);
    }
    public IQueryable CreateQuery(Expression expression)
    {
        var specializedExpression = QueryExpressionSpecializer.Specialize(expression);
        return _actualQueryProvider.CreateQuery(specializedExpression);
    }
    public TResult Execute<TResult>(Expression expression)
    {
        return _actualQueryProvider.Execute<TResult>(expression);
    }
    public object Execute(Expression expression)
    {
        return _actualQueryProvider.Execute(expression);
    }
}

Шаг 3. Наконец, реализуем вспомогательный класс с именем QueryExpressionSpecializer, который заменил бы DateTime.Date на DbFunctions.TruncateTime.

public static class QueryExpressionSpecializer
{
    private static readonly MethodInfo _s_dbFunctions_TruncateTime_NullableOfDateTime = 
        GetMethodInfo<Expression<Func<DateTime?, DateTime?>>>(d => DbFunctions.TruncateTime(d));

    private static readonly PropertyInfo _s_nullableOfDateTime_Value =
        GetPropertyInfo<Expression<Func<DateTime?, DateTime>>>(d => d.Value);

    public static Expression Specialize(Expression general)
    {
        var visitor = new SpecializingVisitor();
        return visitor.Visit(general);
    }
    private static MethodInfo GetMethodInfo<TLambda>(TLambda lambda) where TLambda : LambdaExpression
    {
        return ((MethodCallExpression)lambda.Body).Method;
    }
    public static PropertyInfo GetPropertyInfo<TLambda>(TLambda lambda) where TLambda : LambdaExpression
    {
        return (PropertyInfo)((MemberExpression)lambda.Body).Member;
    }

    private class SpecializingVisitor : ExpressionVisitor
    {
        protected override Expression VisitMember(MemberExpression node)
        {
            if ( node.Expression.Type == typeof(DateTime?) && node.Member.Name == "Date" )
            {
                return Expression.Call(_s_dbFunctions_TruncateTime_NullableOfDateTime, node.Expression);
            }

            if ( node.Expression.Type == typeof(DateTime) && node.Member.Name == "Date" )
            {
                return Expression.Property(
                    Expression.Call(
                        _s_dbFunctions_TruncateTime_NullableOfDateTime, 
                        Expression.Convert(
                            node.Expression, 
                            typeof(DateTime?)
                        )
                    ),
                    _s_nullableOfDateTime_Value
                );
            }

            return base.VisitMember(node);
        }
    }
}

Конечно, приведенная выше реализация QueryExpressionSpecializer может быть обобщена, чтобы позволить подключать любое количество дополнительных преобразований, позволяя членам пользовательских типов использоваться в запросах LINQ, даже если они не известны платформе Entity Framework.

Ответ 5

Хм, не уверен, но не мог ли ты сделать что-то вроде этого?

context.Setup(s => DbFunctions.TruncateTime(It.IsAny<DateTime>()))
    .Returns<DateTime?>(new Func<DateTime?,DateTime?>(
        (x) => {
            /*  whatever modification is required here */
            return x; //or return modified;
        }));

Ответ 6

так как я недавно попал в ту же проблему и выбрал более простое решение, захотел опубликовать его здесь. Это решение не требует никаких Shims, Mocking, ничего экспансивного и т.д.

  • Передайте логический флаг 'useDbFunctions' в ваш метод со значением по умолчанию как true.
  • Когда ваш живой код исполняется, ваш запрос будет использовать DbFunctions, и все будет работать. Из-за значения по умолчанию вызывающим абонентам не нужно беспокоиться об этом.
  • Когда ваш модуль проверяет метод проверки, он может использовать useDbFunctions: false.
  • В вашем методе вы можете использовать флаг для создания своего IQueryable. если useDbFunctions истинно, используйте DbFunctions, чтобы добавить предикат к запросу. если useDbFunctions является ложным, затем пропустите вызов метода DbFunctions и выполните явное эквивалентное решение С#.

Таким образом, ваши модульные тесты будут проверять почти 95% вашего метода в паритете с живым кодом. У вас все еще есть дельта "DbFunctions" по сравнению с вашим эквивалентным кодом, но будьте внимательны к этому, и 95% будут выглядеть как много выигрыша.

public System.Data.DataTable GetLinkedUsers(int parentUserId, bool useDbFunctions = true)
{
    var today = DateTime.Now.Date;

    var queryable = from up in DB.par_UserPlacement
                where up.MentorId == mentorUserId;

    if (useDbFunctions) // use the DbFunctions
    {
     queryable = queryable.Where(up => 
     DbFunctions.TruncateTime(today) >= DbFunctions.TruncateTime(up.StartDate)
     && DbFunctions.TruncateTime(today) <= DbFunctions.TruncateTime(up.EndDate));
    }  
    else
    {
      // do db-functions equivalent here using C# logic
      // this is what the unit test path will invoke
      queryable = queryable.Where(up => up.StartDate < today);
    }                    

    var query = from up in queryable
                select new
                {
                    up.UserPlacementId,
                    up.Users.UserId,
                    up.Users.FirstName,
                    up.Users.LastName,
                    up.Placements.PlacementId,
                    up.Placements.PlacementName,
                    up.StartDate,
                    up.EndDate,
                };

    query = query.OrderBy(up => up.EndDate);

    return this.RunQueryToDataTable(query);
}

Единичные тесты будут ссылаться на mthod как:

GetLinkedUsers(parentUserId: 10, useDbFunctions: false);

Поскольку в модульных тестах были установлены локальные объекты DbContext, функции С# logic/DateTime будут работать.

Ответ 7

Использование Mocks закончилось когда-то. Не макет, просто подключитесь к реальной БД. Регенерировать /Seed DB при запуске теста. Если вы все еще хотите использовать mocks, тогда создайте свой собственный метод, как показано ниже. ИТ изменяет время выполнения поведения. При использовании реального БД он использует функции БД, иначе этот метод. Замените метод DBfunctions в коде с помощью этого метода

public static class CanTestDbFunctions
{
    [System.Data.Entity.DbFunction("Edm", "TruncateTime")]
    public static DateTime? TruncateTime(DateTime? dateValue)
    {
        ...
    }
}

Это - настоящая функция, которая вызывается. И помните, что время не может быть удалено из объекта DateTime, жить в полночь или создавать эквивалент строки.