如何模拟实体框架的FromSqlRaw方法?

问题描述

我正在编写一个单元测试,需要模拟实体框架的.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 );
}
,

内存提供程序无法执行此操作,因为它是一种关系操作。忽略它的哲学方面,可能有两种方法可以解决它。

  1. 模拟查询提供程序

在幕后,它是通过IQueryProvider.CreateQuery<T>(Expression expression)方法运行的,因此您可以使用模拟框架来拦截调用并返回所需的内容。 EntityFrameworkCore.Testing就是这样(免责声明,我是作者)does it。这就是我对代码中的FromSql*调用进行单元测试的方式。

  1. 更好的内存提供程序

我没有使用太多,但是我的理解是像SQLite这样的提供程序可能支持它。

要解决OP的评论,WRT是否应使用内存提供程序/模拟DbContext,我们属于个人观点。我的意思是,我对使用内存中提供程序没有保留,它易于使用,相当快并且对许多人都适用。我确实同意您不应该嘲笑DbContext,只是因为这样做真的很难。 EntityFrameworkCore.Testing本身并不模拟DbContext,它包装了内存中的提供程序,并使用流行的模拟框架为FromSql*ExecuteSql*之类的内容提供支持。 / p>

我阅读了吉米·博加德(Jimmy Bogard)的链接文章(他对此有最大的敬意),但是,在这个主题上,我并不一致。在极少数情况下,我的数据访问层中包含原始SQL,通常是要调用已在SUT外部进行过测试/进行过测试的存储过程或函数。我通常将它们视为依赖项。我应该能够为我的SUT编写单元测试,并且该依赖项返回足够测试我的SUT所需的值。