如何最小起订量实体框架 SqlQuery 调用

Posted

技术标签:

【中文标题】如何最小起订量实体框架 SqlQuery 调用【英文标题】:How to Moq Entity Framework SqlQuery calls 【发布时间】:2014-11-18 19:59:10 【问题描述】:

我已经能够使用这个link 模拟实体框架中的DbSet 和 Moq。

但是,我现在想知道如何模拟对 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

【问题讨论】:

【参考方案1】:

Database.SqlQuery&lt;T&gt; 未标记为虚拟,但Set&lt;T&gt;.SqlQuery 标记为虚拟。

基于Database.SqlQuery&lt;T&gt; 文档

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

Set&lt;T&gt;.SqlQuery 文档

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

那么Database.SqlQuery&lt;T&gt;(String, Object[]) 应该与Set&lt;T&gt;.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】:

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

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

【讨论】:

【参考方案3】:

Database 属性和SqlQuery 方法未标记为virtual,因此它们can't be mocked(使用最小起订量;您可以使用different library 可以解决此问题,但可能比您更具惯性'喜欢)。

您需要使用某种抽象来解决这个问题,例如将数据库的整个查询包装在一个帮助器类中:

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 上的测试覆盖率,但现在查询是so simple there are obviously no deficiencies。

【讨论】:

【参考方案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;
    

    我所有的数据库访问都是通过异步方法进行的。在尝试模拟它时会带来一系列全新的问题。幸运的是 - 已回答 here. 您得到的异常与 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;
                );
    

【讨论】:

以上是关于如何最小起订量实体框架 SqlQuery 调用的主要内容,如果未能解决你的问题,请参考以下文章

起订量和设置数据库上下文

如何最小起订量委托回调和 lambda 方法

使用最小起订量模拟静态属性

使用最小起订量测试 Polly 重试策略

起订量,严格与宽松的使用

无法为 MediatR 设置起订量回调