[英]How to mock Entity Framework's FromSqlRaw method?
I am writing a Unit Test and need to mock Entity Framework's .FromSqlRaw method.我正在编写单元测试,需要模拟实体框架的 .FromSqlRaw 方法。 When the method is executed in the class under test, it throws following exception:
在被测类中执行该方法时,会抛出以下异常:
System.InvalidOperationException: There is no method 'FromSqlOnQueryable' on type 'Microsoft.EntityFrameworkCore.RelationalQueryableExtensions' that matches the specified arguments.
System.InvalidOperationException:在类型“Microsoft.EntityFrameworkCore.RelationalQueryableExtensions”上没有与指定参数匹配的方法“FromSqlOnQueryable”。
Following is class under test:以下是被测类:
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;
}
}
Unit Test:单元测试:
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();
}
}
With .FromSqlRaw
you are sending raw sql query to the database engine.使用
.FromSqlRaw
您将原始 sql 查询发送到数据库引擎。
If you really want to test that your application ( .FromsqlRaw
) works as expected, test it against an actual database.如果您真的想测试您的应用程序 (
.FromsqlRaw
) 是否按预期工作,请针对实际数据库对其进行测试。
Yes it is slower, yes it requires running database with some test data - and yes it will provide you strong confidence that your application is working.是的,它更慢,是的,它需要使用一些测试数据运行数据库 - 是的,它会让您确信您的应用程序正在运行。
All other tests (mocked or in-memory or sqlite) will provide you false feeling of confidence.所有其他测试(模拟的、内存中的或 sqlite 的)都会给你错误的信心。
In my scenario I use FromSqlRaw
method for invoke stored procedure in my database.在我的场景中,我使用
FromSqlRaw
方法调用数据库中的存储过程。 For EntityFramework Core (version 3.1 works well for sure) I do it in this way:对于 EntityFramework Core(3.1 版肯定可以很好地工作),我是这样做的:
Add virtual method to your DbContext
class:将虚拟方法添加到您的
DbContext
类:
public virtual IQueryable<TEntity> RunSql<TEntity>(string sql, params object[] parameters) where TEntity : class
{
return this.Set<TEntity>().FromSqlRaw(sql, parameters);
}
It's just a simple virtaul wraper from static FromSqlRaw
, so you can easily mock it:它只是一个来自静态
FromSqlRaw
的简单虚拟包装FromSqlRaw
,因此您可以轻松模拟它:
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 );
Call our new RunSql
method instead of FromSqlRaw
:调用我们新的
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}, {1})", 4, 5).ToListAsync();
Last, but not least, you need to add AsAsyncQueryable()
extension method to your test project.最后但并非最不重要的是,您需要将
AsAsyncQueryable()
扩展方法添加到您的测试项目中。 It's provided by user @vladimir in a brilliant answer here :它由用户@vladimir 在这里提供了一个精彩的答案:
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 );
}
The in-memory provider can't do it as it's a relational operation.内存中的提供者不能这样做,因为它是一个关系操作。 Ignoring the philosophical side of it there are probably a couple of ways you could solve it.
忽略它的哲学方面,可能有几种方法可以解决它。
Under the covers it's runs through the IQueryProvider.CreateQuery<T>(Expression expression)
method so you can use a mocking framework to intercept the invocation and return what you want.在
IQueryProvider.CreateQuery<T>(Expression expression)
它通过IQueryProvider.CreateQuery<T>(Expression expression)
方法运行,因此您可以使用模拟框架来拦截调用并返回您想要的内容。 That's how EntityFrameworkCore.Testing (disclaimer I am the author) does it .这就是EntityFrameworkCore.Testing (免责声明我是作者)的 方式。 This is how I unit test
FromSql*
invocations in my code.这就是我在代码中对
FromSql*
调用进行单元测试的FromSql*
。
I haven't used it much but my understanding is a provider like SQLite may support it.我没有经常使用它,但我的理解是像 SQLite 这样的提供者可能会支持它。
To address the OP comments, WRT whether you should be using an in-memory provider/mocking the DbContext
, we are in the realm of personal opinion.为了解决 OP 评论,WRT 是否应该使用内存中提供程序/
DbContext
,我们属于个人意见领域。 Mine is that I have no reservations using the in-memory provider, it's easy to use, reasonably fast and works well for many.我的一点是,我对使用内存中的提供程序没有任何保留,它易于使用、速度相当快并且对许多人来说效果很好。 I do agree that you shouldn't mock the
DbContext
, simply because it'd be really hard to do.我同意你不应该嘲笑
DbContext
,仅仅因为它真的很难做到。 EntityFrameworkCore.Testing doesn't mock the DbContext
per se, it wraps over an in-memory provider and uses popular mocking frameworks to provide support for things like FromSql*
and ExecuteSql*
. EntityFrameworkCore.Testing本身并不模拟
DbContext
,它封装了一个内存中的提供程序并使用流行的模拟框架来提供对诸如FromSql*
和ExecuteSql*
。
I read the linked article by Jimmy Bogard (who I have the utmost respect for), however on this topic I don't agree on all points.我阅读了 Jimmy Bogard(我非常尊重他)的链接文章,但是在这个话题上,我并不同意所有观点。 On the rare occasion that I have raw SQL in my data access layer, it's generally to invoke a stored procedure or function which already has been tested/has tests outside of my SUT.
在我的数据访问层中有原始 SQL 的极少数情况下,通常是调用已经在我的 SUT 之外测试/已经测试的存储过程或函数。 I generally treat them as a dependency;
我通常将它们视为依赖项; I should be able to write my unit tests for my SUT with that dependency returning the values required to adequately test my SUT.
我应该能够为我的 SUT 编写单元测试,该依赖项返回充分测试我的 SUT 所需的值。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.