简体   繁体   中英

How to mock Entity Framework's FromSqlRaw method?

I am writing a Unit Test and need to mock Entity Framework's .FromSqlRaw method. 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.

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.
If you really want to test that your application ( .FromsqlRaw ) works as expected, test it against an actual database.

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.

In my scenario I use FromSqlRaw method for invoke stored procedure in my database. For EntityFramework Core (version 3.1 works well for sure) I do it in this way:

Add virtual method to your DbContext class:

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:

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 :

// 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. It's provided by user @vladimir in a brilliant answer 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 );
}

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.

  1. Mocking the query provider

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. That's how EntityFrameworkCore.Testing (disclaimer I am the author) does it . This is how I unit test FromSql* invocations in my code.

  1. A better in-memory provider

I haven't used it much but my understanding is a provider like SQLite may support it.

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. 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. 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* .

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. 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. 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM