[英]How to mock Entity Framework's FromSqlRaw method?
我正在編寫單元測試,需要模擬實體框架的 .FromSqlRaw 方法。 在被測類中執行該方法時,會拋出以下異常:
System.InvalidOperationException:在類型“Microsoft.EntityFrameworkCore.RelationalQueryableExtensions”上沒有與指定參數匹配的方法“FromSqlOnQueryable”。
以下是被測類:
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
的簡單虛擬包裝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 );
調用我們新的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();
最后但並非最不重要的是,您需要將AsAsyncQueryable()
擴展方法添加到您的測試項目中。 它由用戶@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 );
}
內存中的提供者不能這樣做,因為它是一個關系操作。 忽略它的哲學方面,可能有幾種方法可以解決它。
在IQueryProvider.CreateQuery<T>(Expression expression)
它通過IQueryProvider.CreateQuery<T>(Expression expression)
方法運行,因此您可以使用模擬框架來攔截調用並返回您想要的內容。 這就是EntityFrameworkCore.Testing (免責聲明我是作者)的 方式。 這就是我在代碼中對FromSql*
調用進行單元測試的FromSql*
。
我沒有經常使用它,但我的理解是像 SQLite 這樣的提供者可能會支持它。
為了解決 OP 評論,WRT 是否應該使用內存中提供程序/ DbContext
,我們屬於個人意見領域。 我的一點是,我對使用內存中的提供程序沒有任何保留,它易於使用、速度相當快並且對許多人來說效果很好。 我同意你不應該嘲笑DbContext
,僅僅因為它真的很難做到。 EntityFrameworkCore.Testing本身並不模擬DbContext
,它封裝了一個內存中的提供程序並使用流行的模擬框架來提供對諸如FromSql*
和ExecuteSql*
。
我閱讀了 Jimmy Bogard(我非常尊重他)的鏈接文章,但是在這個話題上,我並不同意所有觀點。 在我的數據訪問層中有原始 SQL 的極少數情況下,通常是調用已經在我的 SUT 之外測試/已經測試的存儲過程或函數。 我通常將它們視為依賴項; 我應該能夠為我的 SUT 編寫單元測試,該依賴項返回充分測試我的 SUT 所需的值。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.