C# 如何模拟实体框架';s FromSqlRaw方法?
我正在编写一个单元测试,需要模拟实体框架的.FromSqlRaw方法。在被测类中执行该方法时,会引发以下异常: System.InvalidOperationException:没有方法 类型上的“FromSqlOnQueryable” “Microsoft.EntityFrameworkCore.RelationalQueryableExtensions”表示 匹配指定的参数 以下是正在测试的类:C# 如何模拟实体框架';s FromSqlRaw方法?,c#,asp.net-core,entity-framework-core,xunit,nsubstitute,C#,Asp.net Core,Entity Framework Core,Xunit,Nsubstitute,我正在编写一个单元测试,需要模拟实体框架的.FromSqlRaw方法。在被测类中执行该方法时,会引发以下异常: System.InvalidOperationException:没有方法 类型上的“FromSqlOnQueryable” “Microsoft.EntityFrameworkCore.RelationalQueryableExtensions”表示 匹配指定的参数 以下是正在测试的类: public class PowerConsumptionRepository : IPower
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;
}
}
公共类PowerConsumptionRepository:IPowerConsumptionRepository
{
私有只读IDatabaseContextu databaseContext;
私有只读IDateTimeHelper\u dateTimeHelper;
公共功耗存储库(IDatabaseContext databaseContext,IDateTimeHelper dateTimeHelper)
{
_databaseContext=databaseContext;
_dateTimeHelper=dateTimeHelper;
}
公共列表GetCurrentPowerConsumption(字符串siteId)
{
var currentDate=\u dateTimeHelper
.ConvertutCTOCLocalDateTime(DateTime.UtcNow,ApplicationConstants.LocalTimeZone)
.ToString(“yyyy-MM-dd”);
var currentDateParameter=新的SqlParameter(“currentDate”,currentDate);
var度量=_databaseContext.IntervalPowerConsumptions
.FromSqlRaw(SqlQuery.CurrentIntervalPowerConsument,currentDateParameter)
.AsNoTracking()
.ToList();
回波测量;
}
}
单元测试:
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();
}
}
公共类功耗存储测试
{
[事实]
公共void TestTest()
{
var数据=新列表
{
新的IntervalCategoryConsumption模型
{
Id=1,
Hvac=10
},
新的IntervalCategoryConsumption模型
{
Id=1,
Hvac=10
}
}.AsQueryable();
var dateTimeHelper=Substitute.For();
dateTimeHelper.ConvertutCTLocalDateTime(Arg.Any(),Arg.Any()).Returns(DateTime.Now);
var mockSet=Substitute.For();
((IQueryable)mockSet.Provider.Returns(data.Provider);
((IQueryable)mockSet.Expression.Returns(data.Expression);
((IQueryable)mockSet.ElementType.Returns(data.ElementType);
((IQueryable)mockSet.GetEnumerator()。返回(data.GetEnumerator());
var context=Substitute.For();
context.intervalpowerconsumpions=(mockSet);
var repo=新的PowerConsumptionRepository(上下文,dateTimeHelper);
var result=repo.getCurrentPowerConsument(Arg.Any());
result.Should().NotBeNull();
}
}
内存中的提供程序无法执行此操作,因为它是一个关系操作。忽略哲学的一面,也许有几种方法可以解决它
IQueryProvider.CreateQuery(表达式)
方法运行,因此您可以使用模拟框架拦截调用并返回所需内容。这就是为什么(免责声明我是作者)。这就是我如何在代码中从SQL*调用单元测试
更好的内存提供程序
我没有太多使用它,但我的理解是像SQLite这样的提供商可能会支持它
为了解决OP评论,请考虑您是否应该使用内存提供程序/模拟DbContext
,我们在个人意见范围内。我的观点是,我对使用内存提供程序没有任何保留,它易于使用,速度相当快,并且对许多人都很好。我同意你不应该嘲笑DbContext
,仅仅因为这真的很难做到。它本身并不模拟DbContext
,而是封装了内存中的提供程序,并使用流行的模拟框架来提供对FromSql*
和ExecuteSql*
之类的支持
我读了Jimmy Bogard(我非常尊敬他)的链接文章,但是在这个话题上,我并不完全同意。在很少的情况下,我的数据访问层中有原始SQL,通常是调用一个存储过程或函数,该存储过程或函数已经过测试/在SUT之外有测试。我通常把他们当作一种依赖;我应该能够为我的SUT编写单元测试,使用该依赖项返回充分测试我的SUT所需的值。使用。从SQLRAW
您将原始sql查询发送到数据库引擎。
如果您确实想测试您的应用程序(.FromsqlRaw
)是否按预期工作,请针对实际数据库进行测试
是的,它是较慢的,是的,它需要运行带有一些测试数据的数据库-是的,它将为您的应用程序工作提供强大的信心
所有其他测试(模拟测试、内存测试或sqlite测试)都会让您产生错误的信心。在我的场景中,我使用SQLRAW的方法调用数据库中的存储过程。
对于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);
}
调用新的RunSql
方法,而不是从sqlraw
调用:
// 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}, {1})", 4, 5).ToListAsync();
To:我们从不试图模仿DbContext或IQueryable。这样做是困难、麻烦和脆弱的。不要这样做。(他们的重点)对。我理解。但这是否意味着我无法对其进行单元测试?我不能使用内存中的数据库,因为。FromSqlRaw
执行SQL查询。那不是问题所在吗?您正在测试一个与数据库高度耦合的类,现在您想抽象该数据库,但发现它非常困难或不可能?要正确地测试PowerConsumptionRepository,您可能应该使用“真实”数据库。我最近偶然发现了吉米·博加德的那篇文章,也许
// 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}, {1})", 4, 5).ToListAsync();
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 );
}