Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to disable eager loading when using InMemoryDatabase

I have an EF.Core 2.1 DataContext which I have not enable lazy loading for.

My configuration looks like this:

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

My tests use the same DataContext but use different options like so:

options.UseInMemoryDatabase(databaseName: "ProjectSpecs")

This is all working fine except that my in memory DataContext is eager loading everything.

If I ask for an entity is it loading all the related objects.

This means if I want to actually load a related property and forget to do so, my tests are passing as the related entity is loaded. But in the real application it is failing due to the .include being forgotten.

Can I make the in memory DataContext behave the same as the real one?

like image 372
4imble Avatar asked Oct 10 '18 12:10

4imble


3 Answers

I was suffering from the same issue and after reading a bunch of articles on the subject I came to the conclusion that the problem is really because the code-under-test is reading from the ChangeTracker where the test code has already assembled the object graph. Armed with that knowledge, I took my DbContext and overrode the SaveChanges method as shown below.

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;
}

By detaching every object in the ChangeTracker it forces the code-under-test to go back to the database instead of pulling the existing object graph from the ChangeTracker.

like image 136
Craig W. Avatar answered Nov 11 '22 13:11

Craig W.


One small, but important point. Use of Include() is eager loading. Getting EF to load entities as and when they are needed is lazy loading. You want to disable lazy loading, so that you can test eager loading - the correct use of Include().

By default, lazy loading is disabled.To enable it in test code, add UseLazyLoadingProxies() to your DbContextOptions just as you would for application code. Except it's probably better not to, precisely so you can test you've got the eager loading right.

The problem here is not strictly that your are using lazy loading, it's that you're using the same DbContext to configure your test data as you are to test it. Thus the data remains in the DbContext, and is not being loaded from the in-memory database at all.

Simply make sure you use a different DbContext for the setup and for the tests. It must, though, have the same database name. In fact, you can use exactly the same options object.

like image 23
Jasper Kent Avatar answered Nov 11 '22 13:11

Jasper Kent


I think Jasper Kent's answer is the better way because non-testing code should know as little as possible that testing even exists (i.e. not messing with SaveChanges because the automatic tests need it).

My implementation of this idea is as follows:

InMemoryTestBase (inherited by any testing class)

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 (an example class using this setup)

This example shows a test of a generic service dealing with getting an entity by its identifier, but by also eagerly loading its details. Fluent assertions are used for the asserts.

Deep/full cloning is required to ensure that test data is never changed and also that no reference is ever shared between the parallel running tests.

[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))
    );
}

Each test creates its own database context so that they run in parallel (default with XUnit).

like image 29
Alexei - check Codidact Avatar answered Nov 11 '22 11:11

Alexei - check Codidact