【问题标题】:How could I Mock the FromSql() method?我怎么能模拟 FromSql() 方法?
【发布时间】:2016-11-21 18:07:02
【问题描述】:

我想知道除了构建一个用于模拟FromSql 的包装器之外还有其他方法吗?我知道这种方法是静态的,但是由于他们将AddEntityFrameworkInMemoryDatabase 之类的东西添加到实体框架核心,我认为可能也有解决方案,我在我的项目中使用 EF Core 1.0.1。

我的最终目标是测试这个方法:

public List<Models.ClosestLocation> Handle(ClosestLocationsQuery message)
{
    return _context.ClosestLocations.FromSql(
        "EXEC GetClosestLocations {0}, {1}, {2}, {3}",
        message.LocationQuery.Latitude,
        message.LocationQuery.Longitude,
        message.LocationQuery.MaxRecordsToReturn ?? 10,
        message.LocationQuery.Distance ?? 10
    ).ToList();
}

我想确保我的查询是使用传递给它的相同对象处理的,基于实体框架 6 中的 this answer 我可以执行以下操作:

[Fact]
public void HandleInvokesGetClosestLocationsWithCorrectData()
{
    var message = new ClosestLocationsQuery
    {
        LocationQuery =
            new LocationQuery {Distance = 1, Latitude = 1.165, Longitude = 1.546, MaxRecordsToReturn = 1}
    };

    var dbSetMock = new Mock<DbSet<Models.ClosestLocation>>();

    dbSetMock.Setup(m => m.FromSql(It.IsAny<string>(), message))
        .Returns(It.IsAny<IQueryable<Models.ClosestLocation>>());

    var contextMock = new Mock<AllReadyContext>();

    contextMock.Setup(c => c.Set<Models.ClosestLocation>()).Returns(dbSetMock.Object);

    var sut = new ClosestLocationsQueryHandler(contextMock.Object);
    var results = sut.Handle(message);

    contextMock.Verify(x => x.ClosestLocations.FromSql(It.IsAny<string>(), It.Is<ClosestLocationsQuery>(y =>
        y.LocationQuery.Distance == message.LocationQuery.Distance &&
        y.LocationQuery.Latitude == message.LocationQuery.Latitude &&
        y.LocationQuery.Longitude == message.LocationQuery.Longitude &&
        y.LocationQuery.MaxRecordsToReturn == message.LocationQuery.MaxRecordsToReturn)));
}

但与 EF 6 中的 SqlQuery&lt;T&gt; 不同,EF Core 中的 FromSql&lt;T&gt; 是静态扩展方法,我问这个问题是因为我认为我可能会从错误的角度解决这个问题,或者可能有比一个包装器,我会很感激对此的任何想法。

【问题讨论】:

  • 在内部,EF 核心 FromSql extension methodIQueriable.Provider 上调用 CreateQuery,您可以查看模拟以实现您想要的。
  • 你找到任何模拟FromSql的解决方案了吗?
  • @YawarMurtaza 不,有 Philippe 的答案,但我没有尝试过。

标签: c# unit-testing entity-framework-core moq


【解决方案1】:

我也遇到了同样的情况,菲利普给出的答案有所帮助,但主要方法是抛出System.ArgumentNullException

来自this link,我终于可以写一些单元测试了……

这是我正在测试的课程:

public class HolidayDataAccess : IHolidayDataAccess
{
    private readonly IHolidayDataContext holDataContext;

    public HolidayDataAccess(IHolidayDataContext holDataContext)
    {
        this.holDataContext = holDataContext;
    }

    public async Task<IEnumerable<HolidayDate>> GetHolidayDates(DateTime startDate, DateTime endDate)
    {
        using (this.holDataContext)
        {
            IList<HolidayDate> dates = await holDataContext.Dates.FromSql($"[dba].[usp_GetHolidayDates] @StartDate = {startDate}, @EndDate = {endDate}").AsNoTracking().ToListAsync();
            return dates;
        }
    }
}

这里是测试方法:

[TestMethod]
public async Task GetHolidayDates_Should_Only_Return_The_Dates_Within_Given_Range()
{
    // Arrange.

        SpAsyncEnumerableQueryable<HolidayDate> dates = new SpAsyncEnumerableQueryable<HolidayDate>();
        dates.Add(new HolidayDate() { Date = new DateTime(2018, 05, 01) });
        dates.Add(new HolidayDate() { Date = new DateTime(2018, 07, 01) });
        dates.Add(new HolidayDate() { Date = new DateTime(2018, 04, 01) });
        dates.Add(new HolidayDate() { Date = new DateTime(2019, 03, 01) });
        dates.Add(new HolidayDate() { Date = new DateTime(2019, 02, 15) });


    var options = new DbContextOptionsBuilder<HolidayDataContext>()
        .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
        .Options;

    HolidayDataContext context = new HolidayDataContext(options);

    context.Dates = context.Dates.MockFromSql(dates);

    HolidayDataAccess dataAccess = new HolidayDataAccess(context);

    //Act.
    IEnumerable<HolidayDate> resutlDates = await dataAccess.GetHolidayDates(new DateTime(2018, 01, 01), new DateTime(2018, 05, 31));

    // Assert.

    Assert.AreEqual(resutlDates.Any(d => d.Date != new DateTime(2019, 03, 01)), true);
    Assert.AreEqual(resutlDates.Any(d => d.Date != new DateTime(2019, 02, 15)), true);

    // we do not need to call this becuase we are using a using block for the context...
    //context.Database.EnsureDeleted();
}

要使用UseInMemoryDatabase,您需要从NuGet 添加Microsoft.EntityFrameworkCore.InMemory 包 辅助类在这里:

public class SpAsyncEnumerableQueryable<T> : IAsyncEnumerable<T>, IQueryable<T>
{
    private readonly IList<T> listOfSpReocrds;

    public Type ElementType => throw new NotImplementedException();

    public IQueryProvider Provider => new Mock<IQueryProvider>().Object;

    Expression IQueryable.Expression => throw new NotImplementedException();

    public SpAsyncEnumerableQueryable()
    {
        this.listOfSpReocrds = new List<T>();
    }        

    public void Add(T spItem) // this is new method added to allow xxx.Add(new T) style of adding sp records...
    {
        this.listOfSpReocrds.Add(spItem);
    }

    public IEnumerator<T> GetEnumerator()
    {
       return this.listOfSpReocrds.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    IAsyncEnumerator<T> IAsyncEnumerable<T>.GetEnumerator()
    {
        return listOfSpReocrds.ToAsyncEnumerable().GetEnumerator();
    }
}

...以及包含 FromSql 方法模拟的 Db 扩展类..

public static class DbSetExtensions
{
    public static DbSet<T> MockFromSql<T>(this DbSet<T> dbSet, SpAsyncEnumerableQueryable<T> spItems) where T : class
    {
        var queryProviderMock = new Mock<IQueryProvider>();
        queryProviderMock.Setup(p => p.CreateQuery<T>(It.IsAny<MethodCallExpression>()))
            .Returns<MethodCallExpression>(x => spItems);

        var dbSetMock = new Mock<DbSet<T>>();
        dbSetMock.As<IQueryable<T>>()
            .SetupGet(q => q.Provider)
            .Returns(() => queryProviderMock.Object);

        dbSetMock.As<IQueryable<T>>()
            .Setup(q => q.Expression)
            .Returns(Expression.Constant(dbSetMock.Object));
        return dbSetMock.Object;
    }
}

希望这会有所帮助!

编辑:重构 SpAsyncEnumerableQueryable 类以具有 Add 方法。摆脱了采用 T 数组的参数化构造。实现了IQueryProvider Provider =&gt; new Mock&lt;IQueryProvider&gt;().Object; 以支持.AsNoTracking()。异步调用 ToList。

【讨论】:

  • 为什么MockFromSql&lt;T&gt;是扩展方法?它似乎实际上并没有对dbSet 做任何事情。
  • 我用一个集合尝试了这个并且工作正常,但是尝试模拟一个具有 FirstOrDefaultAsync 的方法会抛出一个异常消息源 IQueryable 的提供程序没有实现 IAsyncQueryProvider
【解决方案2】:

如果您查看FromSql&lt;T&gt; 中的代码,您会发现它调用了source.Provider.CreateQuery&lt;TEntity&gt;。这是你必须模拟的。

在你的情况下,我认为你可以用这样的方法来解决:

var mockProvider = new Mock<IQueryProvider>();
mockProvider.Setup(s => s.CreateQuery(It.IsAny<MethodCallExpression>()))
    .Returns(null as IQueryable);
var mockDbSet = new Mock<DbSet<AllReady.Models.ClosestLocation>>();
mockDbSet.As<IQueryable<AllReady.Models.ClosestLocation>>()
    .Setup(s => s.Provider)
    .Returns(mockProvider.Object);
var t = mockDbSet.Object;
context.ClosestLocations = mockDbSet.Object;

var sut = new ClosestLocationsQueryHandler(context);
var results = sut.Handle(message);

不确定之后如何在MethodCallExpression 上使用Verify,但我想这是可能的。或者,可能有一种方法可以检查生成的 SQL。

【讨论】:

    【解决方案3】:

    旧线程是旧的;但是,如果其他人正在寻找; 可以对 from sql 调用执行 Moq 验证,您必须针对查询提供程序执行此操作。

    var providerMock = Mock.Get(((IQueryable<TestEntity>) mockedDbContext.Set<TestEntity>()).Provider);
    providerMock.Verify(m => m.CreateQuery<TestEntity>(It.IsAny<MethodCallExpression>()), Times.Once);
    

    同样的方法也适用于 NSub。

    方法调用表达式验证可以微调以匹配 sql 和参数,它包含 sql (RawSqlString) 和参数 (IEnumerable) 作为常量表达式。

    var mceRawSqlString = (RawSqlString) ((ConstantExpression) mce.Arguments[1]).Value;
    var mceParameters = (object[]) ((ConstantExpression) mce.Arguments[2]).Value
    

    其中 mce 是馈送到 CreateQueryMethodCallExpression

    我在我的项目EntityFrameworkCore.Testing 中做了一点 MCE 解构以支持 FromSql 模拟;我将实际匹配外包给支持方法,因为它有点毛茸茸。

    queryProviderMock.Setup(m => m.CreateQuery<T>(It.Is<MethodCallExpression>(mce => SpecifiedParametersMatchMethodCallExpression(mce, sqlToMatchAgainst, parametersToMatchAgainst))))
    

    【讨论】:

      猜你喜欢
      • 2011-01-31
      • 2015-10-30
      • 1970-01-01
      • 1970-01-01
      • 2015-04-22
      • 1970-01-01
      • 2020-01-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多