如何Moq实体框架SqlQuery调用

本文关键字:SqlQuery 调用 框架 实体 Moq 如何 | 更新日期: 2023-09-27 18:24:07

我已经能够使用这个链接从Moq的实体框架中模拟DbSet

但是,我现在想知道如何模拟对SqlQuery的调用。不确定这是否可能,也不确定这是如何实现的,因为它依赖于知道调用了什么"查询"的模拟数据库上下文。

下面是我想嘲笑的。

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

我目前还没有尝试过任何东西,因为我不知道如何开始嘲笑这个例子。

下面是对DbSet的模拟,为了重新迭代,我可以正确地模拟返回MyObjectDbSet,但现在正在尝试模拟返回MyObject的列表的SqlQuery。

var dbContext = new Mock<MyDbContext>();
dbContext.Setup(m => m.MyObjects).Returns(mockObjects.Object);
dbContext.Setup(m => m.Database.SqlQuery... something along these lines

如何Moq实体框架SqlQuery调用

Database.SqlQuery<T>未标记为虚拟,但Set<T>.SqlQuery标记为虚拟。

基于Database.SqlQuery<T>文档

上下文永远不会跟踪此查询的结果,即使返回的对象类型是实体类型。使用"SqlQuery(String,Object[])的方法返回由上下文

Set<T>.SqlQuery文档

默认情况下,返回的实体由上下文跟踪;这个罐子通过对返回的DbRawSqlQuery调用AsNoTracking来更改。

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

您可以将一个虚拟方法添加到您的数据库上下文中,您可以在单元测试中覆盖该方法:

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

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的测试覆盖范围,但现在的查询非常简单,显然没有任何不足。

如果有人遇到这个问题。我用几种方法解决了这个问题。只是解决这个问题的另一种方法。

  1. 我的上下文是通过一个接口抽象出来的。我只需要几种方法:

    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;
    

    }

  2. 我所有的数据库访问都是通过异步方法进行的。当试图嘲笑它时,它带来了一系列全新的问题。幸运的是,它在这里得到了回答。您得到的异常与IDbSyncEnumerable缺少的mock有关。使用提供的解决方案-我只是对其进行了更多的扩展,这样我就有了一个助手来返回一个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;
    }
    
  3. 最后,使用@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;
                });
    

我对@Patrick Quirk的答案做了一个变体,对我来说效果很好,我将上下文传递到方法本身中。我的界面:

public interface ISqlQueryHelper<T, in TU> where T : class where TU : DbContext
    {
        Task<IList<T>> Execute(FormattableString value, TU context);
    }

实现:

public class SqlQueryHelper<T,TU> : ISqlQueryHelper<T,TU> where T : class 
                                                            where TU : DbContext
    {
        public async Task<IList<T>> Execute(FormattableString value, TU context)
        {
            return await context.Database.SqlQuery<T>(value).ToListAsync();
        }
    }

在Startup.cs:中连接依赖项注入

services.AddScoped<ISqlQueryHelper<UserDto,UserContext>, SqlQueryHelper<UserDto,UserContext>>();

然后最后给它打电话:

FormattableString query = $"EXECUTE [dbo].[GetTechLeadForSpecifiedUser] @userId = {userId}";
            var techLeads = (await _sqlQueryHelper.Execute(query, _context)).ToList();