问题描述
我正在编写一个单元测试,需要模拟实体框架的.FromsqlRaw方法。当该方法在被测类中执行时,将引发以下异常:
system.invalidOperationException:没有方法 'FromsqlOnQueryable'类型 “ Microsoft.EntityFrameworkCore.RelationalQueryableExtensions” 匹配指定的参数。
以下是受测类:
public class PowerConsumptionRepository : IPowerConsumptionRepository
{
private readonly IDatabaseContext _databaseContext;
private readonly IDateTimeHelper _dateTimeHelper;
public PowerConsumptionRepository(IDatabaseContext databaseContext,IDateTimeHelper dateTimeHelper)
{
_databaseContext = databaseContext;
_dateTimeHelper = dateTimeHelper;
}
public List<IntervalCategoryConsumptionModel> GetCurrentPowerConsumption(string siteId)
{
var currentDate = _dateTimeHelper
.ConvertUtcToLocalDateTime(DateTime.UtcNow,ApplicationConstants.LocalTimeZone)
.ToString("yyyy-MM-dd");
var currentDateParameter = new sqlParameter("currentDate",currentDate);
var measurements = _databaseContext.IntervalPowerConsumptions
.FromsqlRaw(sqlQuery.CurrentIntervalPowerConsumption,currentDateParameter)
.AsNoTracking()
.ToList();
return measurements;
}
}
单元测试:
public class PowerConsumptionRepositoryTests
{
[Fact]
public void Testtest()
{
var data = new List<IntervalCategoryConsumptionModel>
{
new IntervalCategoryConsumptionModel
{
Id = 1,Hvac = 10
},new IntervalCategoryConsumptionModel
{
Id = 1,Hvac = 10
}
}.AsQueryable();
var dateTimeHelper = Substitute.For<IDateTimeHelper>();
dateTimeHelper.ConvertUtcToLocalDateTime(Arg.Any<DateTime>(),Arg.Any<string>()).Returns(DateTime.Now);
var mockSet = Substitute.For<DbSet<IntervalCategoryConsumptionModel>,IQueryable<IntervalCategoryConsumptionModel>>();
((IQueryable<IntervalCategoryConsumptionModel>)mockSet).Provider.Returns(data.Provider);
((IQueryable<IntervalCategoryConsumptionModel>)mockSet).Expression.Returns(data.Expression);
((IQueryable<IntervalCategoryConsumptionModel>)mockSet).ElementType.Returns(data.ElementType);
((IQueryable<IntervalCategoryConsumptionModel>)mockSet).GetEnumerator().Returns(data.GetEnumerator());
var context = Substitute.For<IDatabaseContext>();
context.IntervalPowerConsumptions = (mockSet);
var repo = new PowerConsumptionRepository(context,dateTimeHelper);
var result = repo.GetCurrentPowerConsumption(Arg.Any<string>());
result.Should().NotBeNull();
}
}
解决方法
使用.FromSqlRaw
,您正在将原始SQL查询发送到数据库引擎。
如果您确实要测试您的应用程序(.FromsqlRaw
)是否可以正常工作,请针对实际数据库进行测试。
是的,它速度较慢,是的,它需要运行包含一些测试数据的数据库,是的,它将为您提供强大的信心,使您的应用程序正常运行。
所有其他测试(模拟,内存或sqlite)都会给您虚假的信心。
,在我的场景中,我使用 FromSqlRaw
方法调用数据库中的存储过程。
对于 EntityFramework Core(当然 3.1 版效果很好),我是这样做的:
向您的 DbContext
类添加虚拟方法:
public virtual IQueryable<TEntity> RunSql<TEntity>(string sql,params object[] parameters) where TEntity : class
{
return this.Set<TEntity>().FromSqlRaw(sql,parameters);
}
它只是一个来自静态 FromSqlRaw
的简单 virtaul 包装器,因此您可以轻松模拟它:
var dbMock = new Mock<YourContext>();
var tableContent = new List<YourTable>()
{
new YourTable() { Id = 1,Name = "Foo" },new YourTable() { Id = 2,Name = "Bar" },}.AsAsyncQueryable();
dbMock.Setup(_ => _.RunSql<YourTable>(It.IsAny<string>(),It.IsAny<object[]>())).Returns(tableContent );
调用我们新的 RunSql
方法而不是 FromSqlRaw
:
// Before
//var resut = dbContext.FromSqlRaw<YourTable>("SELECT * FROM public.stored_procedure({0},{1})",4,5).ToListAsync();
// New
var result = dbContext.RunSql<YourTable>("SELECT * FROM public.stored_procedure({0},5).ToListAsync();
最后但并非最不重要的是,您需要将 AsAsyncQueryable()
扩展方法添加到您的测试项目中。它由用户@vladimir 在一个精彩的答案中提供here:
public static class QueryableExtensions
{
public static IQueryable<T> AsAsyncQueryable<T>(this IEnumerable<T> input)
{
return new NotInDbSet<T>( input );
}
}
public class NotInDbSet< T > : IQueryable<T>,IAsyncEnumerable< T >,IEnumerable< T >,IEnumerable
{
private readonly List< T > _innerCollection;
public NotInDbSet( IEnumerable< T > innerCollection )
{
_innerCollection = innerCollection.ToList();
}
public IAsyncEnumerator< T > GetAsyncEnumerator( CancellationToken cancellationToken = new CancellationToken() )
{
return new AsyncEnumerator( GetEnumerator() );
}
public IEnumerator< T > GetEnumerator()
{
return _innerCollection.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public class AsyncEnumerator : IAsyncEnumerator< T >
{
private readonly IEnumerator< T > _enumerator;
public AsyncEnumerator( IEnumerator< T > enumerator )
{
_enumerator = enumerator;
}
public ValueTask DisposeAsync()
{
return new ValueTask();
}
public ValueTask< bool > MoveNextAsync()
{
return new ValueTask< bool >( _enumerator.MoveNext() );
}
public T Current => _enumerator.Current;
}
public Type ElementType => typeof( T );
public Expression Expression => Expression.Empty();
public IQueryProvider Provider => new EnumerableQuery<T>( Expression );
}
,
内存提供程序无法执行此操作,因为它是一种关系操作。忽略它的哲学方面,可能有两种方法可以解决它。
- 模拟查询提供程序
在幕后,它是通过IQueryProvider.CreateQuery<T>(Expression expression)
方法运行的,因此您可以使用模拟框架来拦截调用并返回所需的内容。 EntityFrameworkCore.Testing就是这样(免责声明,我是作者)does it。这就是我对代码中的FromSql*
调用进行单元测试的方式。
- 更好的内存提供程序
我没有使用太多,但是我的理解是像SQLite这样的提供程序可能支持它。
要解决OP的评论,WRT是否应使用内存提供程序/模拟DbContext
,我们属于个人观点。我的意思是,我对使用内存中提供程序没有保留,它易于使用,相当快并且对许多人都适用。我确实同意您不应该嘲笑DbContext
,只是因为这样做真的很难。 EntityFrameworkCore.Testing本身并不模拟DbContext
,它包装了内存中的提供程序,并使用流行的模拟框架为FromSql*
和ExecuteSql*
之类的内容提供支持。 / p>
我阅读了吉米·博加德(Jimmy Bogard)的链接文章(他对此有最大的敬意),但是,在这个主题上,我并不一致。在极少数情况下,我的数据访问层中包含原始SQL,通常是要调用已在SUT外部进行过测试/进行过测试的存储过程或函数。我通常将它们视为依赖项。我应该能够为我的SUT编写单元测试,并且该依赖项返回足够测试我的SUT所需的值。