【问题标题】:How to mock Entity Framework's FromSqlRaw method?如何模拟实体框架的 FromSqlRaw 方法?
【发布时间】:2020-10-11 09:01:13
【问题描述】:

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

【问题讨论】:

  • quote the EF Core documentation我们从不尝试模拟 DbContext 或 IQueryable。这样做是困难的、麻烦的和脆弱的。 不要这样做。(他们的重点。)
  • 对。我明白。但这是否意味着我不能对其进行单元测试?我无法使用内存数据库,因为.FromSqlRaw 执行 SQL 查询。
  • 那不是问题吗?您正在测试一个与您的数据库高度耦合的类,现在您想抽象该数据库但发现它非常困难或不可能?要正确测试PowerConsumptionRepository,您可能应该使用“真实”数据库。我最近看到了 Jimmy Bogard 的帖子Avoid In-Memory Databases for Tests,它可能会让你对这个问题有更多的看法。
  • 或者检查生产代码中的提供者类型。

标签: c# asp.net-core entity-framework-core xunit nsubstitute


【解决方案1】:

在我的场景中,我使用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}, {1})", 4, 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 );
}

【讨论】:

    【解决方案2】:

    使用.FromSqlRaw,您正在向数据库引擎发送原始 sql 查询。
    如果您真的想测试您的应用程序 (.FromsqlRaw) 是否按预期工作,请针对实际数据库进行测试。

    是的,它比较慢,是的,它需要运行带有一些测试数据的数据库 - 是的,它会让您确信您的应用程序正在运行。

    所有其他测试(模拟测试、内存测试或 sqlite 测试)会给您带来虚假的自信感。

    【讨论】:

      【解决方案3】:

      内存提供程序不能这样做,因为它是一个关系操作。忽略它的哲学方面,您可能有几种方法可以解决它。

      1. 模拟查询提供程序

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

      1. 更好的内存提供程序

      我用的不多,但我的理解是像 SQLite 这样的提供者可能支持它。

      为了解决 OP cmets,WRT 是否应该使用内存提供程序/模拟 DbContext,我们处于个人意见的范围内。我的是,我对使用内存提供程序没有任何保留,它易于使用,速度相当快,并且适用于许多人。我同意你不应该嘲笑DbContext,仅仅因为它真的很难做到。 EntityFrameworkCore.Testing 本身并不模拟 DbContext,它封装了一个内存提供程序,并使用流行的模拟框架来为 FromSql*ExecuteSql* 之类的东西提供支持。

      我阅读了 Jimmy Bogard(我非常尊重他)的链接文章,但是关于这个主题,我并不同意所有观点。在我的数据访问层中有原始 SQL 的极少数情况下,通常是调用已经在我的 SUT 之外测试过的存储过程或函数。我通常将它们视为依赖项;我应该能够为我的 SUT 编写单元测试,该依赖项返回充分测试我的 SUT 所需的值。

      【讨论】:

      • 我已经尝试过您的解决方案,但我能得到的只是异常:'找不到类型'MyProject.Models.TestDbContext'的构造函数。',出了什么问题,因为我的 TestDbContext 有一个公共的无参数构造函数 (public TestDbContext() { })。
      • 如果您在使用我的库时遇到问题,请在 repo 中使用工作示例弹出问题,我可以进一步帮助您。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多