使用InMemoryDatabase时如何禁用预加载

时间:2018-10-10 12:47:58

标签: entity-framework entity-framework-core

我有一个EF.Core 2.1 DataContext,但尚未为其启用延迟加载。

我的配置如下:

services.AddDbContext<DataContext>(options =>  
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

我的测试使用相同的DataContext,但是使用不同的选项,例如:

options.UseInMemoryDatabase(databaseName: "ProjectSpecs")

一切正常,除了我的内存DataContext渴望加载所有内容。

如果我要一个实体,它将加载所有相关对象。

这意味着,如果我想实际加载相关属性而忘记这样做,那么在加载相关实体时我的测试就会通过。但是在实际的应用程序中,由于.include被遗忘而导致失败。

我可以使内存中的DataContext行为与真实的行为相同吗?

3 个答案:

答案 0 :(得分:0)

我遇到了同样的问题,在阅读了许多有关该主题的文章后,我得出的结论是,问题确实是因为被测代码是从ChangeTracker中读取的,其中测试代码已经汇编了ChangeTracker。对象图。有了这些知识,我就使用了DbContext并覆盖了SaveChanges方法,如下所示。

public override int SaveChanges()
{
    var affectedRows = base.SaveChanges();

    if (Database.ProviderName == "Microsoft.EntityFrameworkCore.InMemory")
    {
        ChangeTracker.Entries()
            .Where(e => e.Entity != null)
            .ToList()
            .ForEach(e => e.State = EntityState.Detached);
    }

    return affectedRows;
}

通过分离ChangeTracker中的每个对象,它将迫使被测代码返回数据库,而不是从ChangeTracker中提取现有的对象图。

答案 1 :(得分:0)

一个小而重要的观点。急需加载Include()了。 懒惰加载使EF可以在需要时加载实体。您想禁用延迟加载,以便测试急切加载-Include()的正确使用。

默认情况下,禁用延迟加载 。要在测试代码中启用它,请像对应用程序代码一样将UseLazyLoadingProxies()添加到DbContextOptions中。除非最好不要这样做,否则您可以测试一下是否已经准备好了急切的加载。

这里的问题并不完全是您使用的是延迟加载,而是您使用的是与测试数据相同的DbContext来配置测试数据。因此,数据保留在DbContext中,根本没有从内存数据库中加载。

只需确保为安装程序和测试使用其他DbContext。但是,它必须具有相同的数据库名称。实际上,您可以使用完全相同的options对象。

答案 2 :(得分:0)

我认为 Jasper Kent 的答案是更好的方法,因为非测试代码应该尽可能少地知道测试是否存在(即不要搞乱 SaveChanges,因为自动测试需要它)。

我对这个想法的实现如下:

InMemoryTestBase(由任何测试类继承)

protected IApplicationDbContext CreateContext()
{
    var options = new DbContextOptionsBuilder<ApplicationDbContext>()
        .UseInMemoryDatabase($"ApplicationDbContext_{Guid.NewGuid()}")
        .EnableSensitiveDataLogging(true)
        .Options;

    var dbContext = new ApplicationDbContext(options);

    Populate(dbContext);
    dbContext.SaveChanges();

    // getting another context instance
    var newContext = new ApplicationDbContext(options);
    return newContext;
}

private void Populate(IApplicationDbContext dbContext)
{
    dbContext.EnsureDeleted();

    // actual insert of test data in the in-memory database
}

GenericContextOperationServiceTests(使用此设置的示例类)

此示例展示了对通用服务的测试,该服务通过标识符获取实体,但也急切加载其详细信息。流利的断言用于断言。

需要深度/完全克隆以确保测试数据永远不会更改,并且在并行运行的测试之间不会共享任何引用。

[Theory]
[InlineData(1)]
[InlineData(2)]
public async Task GetByIdWithIncludeReturnsEntityWithDetails(int entityId)
{
    var dbContext = CreateContext();

    var includes = new List<Expression<Func<MockModel, object>>> { e => e.MockModelDetail };
    var entity = await Instance(dbContext).GetById(entityId, includes);

    var expectedHeader = MockModelTestData.MockModelData.FirstOrDefault(e => e.Id == entityId);
    var expectedDetails = MockModelTestData.MockModelDetailData.Where(md => md.MockModelId == entityId).DeepClone().ToList();
    
    entity.Should().BeEquivalentTo(expectedHeader, 
        opt => opt.Including(e => e.Id).Including(e => e.Name));

    entity.MockModelDetail.ForEach(md =>
        md.Should().BeEquivalentTo(expectedDetails.First(ed => ed.Id == md.Id),
            opt => opt.Including(e => e.DetailName).Including(e => e.DateCreation))
    );
}

每个测试都会创建自己的数据库上下文,以便它们并行运行(XUnit 的默认设置)。