Как выполнить транзакцию по методу Moq Entity Framework SqlQuery

Я смог издеваться над DbSet из фреймворка сущности с помощью Moq, используя эту ссылку .

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

Ниже я пытаюсь высмеять.

var myObjects = DbContext.Database
    .SqlQuery<MyObject>("exec [dbo].[my_sproc] {0}", "some_value")
    .ToList();

Я в настоящее время ничего не пробовал, так как не знал, как начать издеваться над этим примером.

Издевка DbSet ниже и повторная итерация, я могу правильно высмеять возврат DbSet из MyObject, но теперь я пытаюсь высмеять SqlQuery, который возвращает список MyObject.

var dbContext = new Mock<MyDbContext>();
dbContext.Setup(m => m.MyObjects).Returns(mockObjects.Object);

dbContext.Setup(m => m.Database.SqlQuery... something along these lines

Ответ 1

Database.SqlQuery<T> не помечен как виртуальный, но Set<T>.SqlQuery отмечен как виртуальный.

На основе Database.SqlQuery<T> документации

Результаты этого запроса никогда не отслеживаются контекстом, даже если Тип возвращаемого объекта - тип объекта. Используйте 'SqlQuery (String, Object []) ' для возврата объектов, которые отслеживаются контекст.

и Set<T>.SqlQuery документация

По умолчанию возвращаемые объекты отслеживаются контекстом; Это может можно изменить, вызвав AsNoTracking в возвращаемом DbRawSqlQuery.

тогда Database.SqlQuery<T>(String, Object[]) должен быть эквивалентен Set<T>.SqlQuery(String, Object[]).AsNoTracking() (только если T является объектом EF, а не DTO/VM).

Итак, если вы можете заменить реализацию на:

var myObjects = DbContext
    .Set<MyObject>()
    .SqlQuery("exec [dbo].[my_sproc] {0}", "some_value")
    .AsNoTracking()
    .ToList();

вы можете издеваться над ним, следуя

var list = new[] 
{ 
    new MyObject { Property = "some_value" },
    new MyObject { Property = "some_value" },
    new MyObject { Property = "another_value" }
};

var setMock = new Mock<DbSet<MyObject>>();
setMock.Setup(m => m.SqlQuery(It.IsAny<string>(), It.IsAny<object[]>()))
    .Returns<string, object[]>((sql, param) => 
    {
        // Filters by property.
        var filteredList = param.Length == 1 
            ? list.Where(x => x.Property == param[0] as string) 
            : list;
        var sqlQueryMock = new Mock<DbSqlQuery<MyObject>>();
        sqlQueryMock.Setup(m => m.AsNoTracking())
            .Returns(sqlQueryMock.Object);
        sqlQueryMock.Setup(m => m.GetEnumerator())
            .Returns(filteredList.GetEnumerator());
        return sqlQueryMock.Object;
    });

var contextMock = new Mock<MyDbContext>();
contextMock.Setup(m => m.Set<MyObject>()).Returns(setMock.Object);

Ответ 2

Database свойство и SqlQuery не отмечены как virtual, поэтому они не могут издеваться (используя Moq; вы можете использовать другая библиотека, которая может объяснить это, но это может быть больше инерции, чем хотелось бы).

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

public interface IQueryHelper
{
    IList<MyObject> DoYourQuery(string value);
}

public class QueryHelper : IQueryHelper
{
    readonly MyDbContext myDbContext;

    public QueryHelper(MyDbContext myDbContext)
    {
        this.myDbContext = myDbContext;
    }

    public IList<MyObject> DoYourQuery(string value)
    {
        return myDbContext.Database.SqlQuery<MyObject>("exec [dbo].[my_sproc] {0}", value).ToList();
    }
}

Теперь метод, который вы тестируете, становится:

public void YourMethod()
{
    var myObjects = queryHelper.DoYourQuery("some_value");
}

Затем вы вводите IQueryHelper в конструкторе тестируемого класса и издеваетесь над этим.

Вы потеряете покрытие теста на DoYourQuery, но теперь запрос настолько прост, что, очевидно, нет недостатков.

Ответ 3

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

public partial class MyDatabaseContext : DbContext
{
    /// <summary>
    /// Allows you to override queries that use the Database property
    /// </summary>
    public virtual List<T> SqlQueryVirtual<T>(string query)
    {
        return this.Database.SqlQuery<T>(query).ToList();
    }
}

Ответ 4

Если кто-нибудь столкнется с этим. Я решил это несколькими подходами. Это еще один способ решить эту проблему.

  • Мой контекст абстрагируется через интерфейс. Мне нужны только некоторые из методов:

    public interface IDatabaseContext
    {
        DbSet<T> Set<T>() where T : class;
        DbEntityEntry<T> Entry<T>(T entity) where T : class;
        int SaveChanges();
        Task<int> SaveChangesAsync();
        void AddOrUpdateEntity<TEntity>(params TEntity[] entities) where TEntity : class;
    

    }

  • Весь доступ к моей базе данных осуществляется с помощью методов async. Что вызывает целый ряд проблем при попытке издеваться над этим. К счастью, здесь был дан ответ . Исключение, которое вы получаете, связано с отсутствующим макетом для IDbAsyncEnumerable. Используя предоставленное решение - я просто расширил его немного больше, чтобы у меня был помощник, чтобы вернуть объект Mock > , издевавшийся над всеми ожидаемыми свойствами.

    public static Mock<DbSqlQuery<TEntity>> CreateDbSqlQuery<TEntity>(IList<TEntity> data)
        where TEntity : class, new()
    {
        var source = data.AsQueryable();
        var mock = new Mock<DbSqlQuery<TEntity>>() {CallBase = true};
        mock.As<IQueryable<TEntity>>().Setup(m => m.Expression).Returns(source.Expression);
        mock.As<IQueryable<TEntity>>().Setup(m => m.ElementType).Returns(source.ElementType);
        mock.As<IQueryable<TEntity>>().Setup(m => m.GetEnumerator()).Returns(source.GetEnumerator());
        mock.As<IQueryable<TEntity>>().Setup(m => m.Provider).Returns(new TestDbAsyncQueryProvider<TEntity>(source.Provider));
        mock.As<IDbAsyncEnumerable<TEntity>>().Setup(m => m.GetAsyncEnumerator()).Returns(new TestDbAsyncEnumerator<TEntity>(data.GetEnumerator()));
        mock.As<IDbSet<TEntity>>().Setup(m => m.Create()).Returns(new TEntity());
        mock.As<IDbSet<TEntity>>().Setup(m => m.Add(It.IsAny<TEntity>())).Returns<TEntity>(i => { data.Add(i); return i; });
        mock.As<IDbSet<TEntity>>().Setup(m => m.Remove(It.IsAny<TEntity>())).Returns<TEntity>(i => { data.Remove(i); return i; });
        return mock;
    }
    
  • Наконец, используя решение, предоставленное @Yulium Chandra, мое тестирование исходного SQL с посмеянным контекстом выглядит следующим образом:

        public Mock<DbSet<TestModel>> MockDbSet { get; }
    
        ....
    
        MockDbSet.Setup(x => x.SqlQuery(It.IsAny<string>))
              .Returns<string,object[]>
              ((sql, param) => 
              {
                    var sqlQueryMock = MockHelper.CreateDbSqlQuery(Models);
    
                    sqlQueryMock.Setup(x => x.AsNoTracking())
                      .Returns(sqlQueryMock.Object);
    
                    return sqlQueryMock.Object;
                });