我已经能够使用这个DbSet
在Moq实体框架中模拟link。
但是,我现在想知道如何模拟对SqlQuery的调用。不确定这是否可行或者如何依赖于模拟的db上下文知道正在调用什么“查询”。
以下是我想要嘲笑的内容。
var myObjects = DbContext.Database
.SqlQuery<MyObject>("exec [dbo].[my_sproc] {0}", "some_value")
.ToList();
我目前还没有尝试任何东西,因为不知道如何开始嘲笑这个例子。
DbSet
的模拟是在下面并重新迭代,我可以正确地模拟返回DbSet
的MyObject
但现在我试图模拟一个返回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
标记为虚拟。
即使返回的对象类型是实体类型,上下文也不会跟踪此查询的结果。使用'SqlQuery(String, Object[])'方法返回由上下文跟踪的实体。
默认情况下,返回的实体由上下文跟踪;这可以通过在返回的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);
Database
属性和SqlQuery
方法没有标记为virtual
所以他们can't be mocked(使用Moq;你可以使用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。
您可以在数据库上下文中添加一个虚拟方法,您可以在单元测试中覆盖它:
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();
}
}
任何人都应该遇到这个。我用几种方法解决了这个问题。另一种解决这个问题的方法。
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;
}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;
}
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;
});