How can I use InMemory database to check records added correctly in a unit test

Viewed 498

Using InMemory database I'm trying to write a unit test which will check that a method is adding a record to a table. The record gets added in the class under test, but when i try and re-create I'm getting context disposed error. What is the correct way of setting up this test so that I can check the table in memory after the context has been disposed?

Unit Test

[Fact]
public void Test_AddContact_AddsSuccessfully()
{
    var contextFactoryMock = new Mock<IContextFactory>();
    contextFactoryMock.Setup(x => x.CreateContext()).Returns(CreateInMemoryContactContext());

    var classUnderTest = new AddContact(contextFactoryMock.Object); 

    var response = classUnderTest.Run(new UkContactUsDto());

    using (var ctx = contextFactoryMock.Object.CreateContext()) 
    {
        var items = ctx.ContactUs.ToList(); //<--- Exception happens here due to ctx being disposed
    }
}

Create InMemory method

private ContactContext CreateInMemoryContactContext()
{
    var _inMemoryContext = new ContactContext(new DbContextOptionsBuilder<ContactContext>().UseInMemoryDatabase((Guid.NewGuid().ToString())).Options);
    
    return _inMemoryContext;
}

Method im testing

try
{
    await using var ctx = _contextFactory.CreateContext();
    ctx.ContactUs.Add(contactUs);
    ctx.SaveChanges();
}
catch (Exception ex)
{
    log.LogInformation(ex,"An error occured during contact us insertion.");
    throw;
}

Error Message

Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.

2 Answers

In the Mock, the method CreateContext is mocked with a instance of DbContext. The Mock return the same instance for all call to CreateContext. You can check with this test :

var context = CreateInMemoryContactContext();
var contextFactoryMock = new Mock<IContextFactory>();
contextFactoryMock.Setup(x => x.CreateContext()).Returns(context);
Assert.ReferenceEquals(context, contextFactoryMock.Object.CreateContext());

In think AddContact dispose the context. When you reuse the context to do assertion, the context is disposed.

You need pass a factory in Returns like :

var contextFactoryMock = new Mock<IContextFactory>();
contextFactoryMock.Setup(x => x.CreateContext()).Returns(CreateInMemoryContactContext);

To conserve data between context, you need reuse the same DbOption when you create new DbContext. With xunit, for each test a instance of test class is instancied. You can put the initialization part in the constructor :

public class AddContactTest
{
    private DbOptions _dbOptions;

    public AddContactTest()
    {
        _options = new DbContextOptionsBuilder<ContactContext>().UseInMemoryDatabase((Guid.NewGuid().ToString())).Options
    }

    private ContactContext CreateInMemoryContactContext()
    {
        var _inMemoryContext = new ContactContext(_dbOptions);
        return _inMemoryContext;
    }

    [Fact]
    public void Test_AddContact_AddsSuccessfully()
    {
        var contextFactoryMock = new Mock<IContextFactory>();
        contextFactoryMock.Setup(x => x.CreateContext()).Returns(CreateInMemoryContactContext);

        var classUnderTest = new AddContact(contextFactoryMock.Object); 

        var response = classUnderTest.Run(new UkContactUsDto());

        using (var ctx = contextFactoryMock.Object.CreateContext()) 
        {
            var items = ctx.ContactUs.ToList();
        }
    }
}

You are disposing the context in the method you are testing so you can't then use it again. One way round it is to create a new context, but you will need to manually keep track of a InMemoryDatabaseRoot instance. For example:

private static readonly _memoryDatabaseRoot InMemoryDatabaseRoot = new InMemoryDatabaseRoot();

private ContactContext CreateInMemoryContactContext()
{
    var _inMemoryContext = new ContactContext(new DbContextOptionsBuilder<ContactContext>()
        .UseInMemoryDatabase("my-database", _memoryDatabaseRoot).Options);
    
    return _inMemoryContext;
}

Now in your test you can call CreateInMemoryContactContext to get a new context that uses the same in-memory database instance

Note: You may not want the root to be static if you're going to use it in multiple tests.

Related