Vin*_*kar 5 c# xunit nsubstitute entity-framework-core asp.net-core
我正在编写单元测试,需要模拟实体框架的 .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;
        }
    }
Run Code Online (Sandbox Code Playgroud)
单元测试:
    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();
        }
    }
Run Code Online (Sandbox Code Playgroud)
    在我的场景中,我使用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);
}
Run Code Online (Sandbox Code Playgroud)
它只是static 的一个简单的虚拟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 );
Run Code Online (Sandbox Code Playgroud)
调用我们的新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();
Run Code Online (Sandbox Code Playgroud)
最后但并非最不重要的一点是,您需要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 );
}
Run Code Online (Sandbox Code Playgroud)
        内存提供程序无法执行此操作,因为它是关系操作。忽略它的哲学方面,可能有几种方法可以解决它。
在幕后,它通过IQueryProvider.CreateQuery<T>(Expression expression)方法运行,因此您可以使用模拟框架来拦截调用并返回您想要的内容。这就是EntityFrameworkCore.Testing(免责声明我是作者)的做法。这就是我FromSql*在代码中对调用进行单元测试的方式。
我没有太多使用它,但我的理解是像 SQLite 这样的提供程序可能支持它。
为了解决 OP 评论,WRT 是否应该使用内存提供程序/嘲笑DbContext,我们处于个人意见领域。我的观点是,我对使用内存提供程序没有任何保留,它易于使用,速度相当快,并且对许多人来说效果很好。我确实同意你不应该嘲笑DbContext,因为这真的很难做到。EntityFrameworkCore.Testing本身并不模拟DbContext,它包装内存中提供程序并使用流行的模拟框架来提供对 和 等内容的FromSql*支持ExecuteSql*。
我阅读了吉米·博加德(Jimmy Bogard)(我非常尊敬他)的链接文章,但是在这个主题上我并不同意所有观点。在极少数情况下,我的数据访问层中有原始 SQL,通常会调用已在 SUT 之外经过测试/已进行测试的存储过程或函数。我通常将它们视为一种依赖;我应该能够为我的 SUT 编写单元测试,并使用该依赖项返回充分测试我的 SUT 所需的值。
|   归档时间:  |  
           
  |  
        
|   查看次数:  |  
           8175 次  |  
        
|   最近记录:  |