如何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
的模拟,为了重新迭代,我可以正确地模拟返回MyObject
的DbSet
,但现在正在尝试模拟返回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
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
的测试覆盖范围,但现在的查询非常简单,显然没有任何不足。
如果有人遇到这个问题。我用几种方法解决了这个问题。只是解决这个问题的另一种方法。
-
我的上下文是通过一个接口抽象出来的。我只需要几种方法:
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;
}
-
我所有的数据库访问都是通过异步方法进行的。当试图嘲笑它时,它带来了一系列全新的问题。幸运的是,它在这里得到了回答。您得到的异常与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; }
-
最后,使用@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();