I hit a problem yesterday when unit testing a service in my application using the in-memory provider for Entity Framework Core. The method I was trying to test was returning an item from the database given it's ID (key). It worked when I ran the test individually, but failed when run with my other tests. Here's how I investigated it and ultimately solved it.

The test

Here's my test class:

[TestClass]
public class UnitTest1 {

    [DataTestMethod]
    [DataRow(1)]
    [DataRow(2)]
    public async Task Get_Document_By_Id_Returns_Result(int id) {

        // ARRANGE: create an in-memory instance of my db context
        var db = new DocumentDbContext(
            new DbContextOptionsBuilder<DocumentDbContext>()
                .UseInMemoryDatabase($"db-{Guid.NewGuid()}")
                .Options
            );

        // ARRANGE: create my service class, passing in the db context
        var service = new DocumentService(
            db
            );

        // ARRANGE: create some test data
        db.Documents.AddRange(new[] {
            new Document{ Title = "Document 1"},
            new Document{ Title = "Document 2"}
        });
        await db.SaveChangesAsync();


        // ACT: get the document by Id
        var result = await service.GetById(id);

        // ASSERT
        Assert.IsNotNull(result);

    }

}

This is a pretty simple test. Assuming that my Document class in my DbContext has an Id property that is the key, we should get an auto incrementing number in there and both my test should pass, right? But, no:

TestFail1

So, what happened?

The first test ran with an Id of 1 and that passed.
Then, the test ran with an Id of 2 and that failed... no document was found with an Id of 2. But why?!

Let's debug the test and find out.

Debugging

To find out what's happening I set a breakpoint in my test with a condition of id == 2 and debugged the test. Now I can take a look at the contents of my db:

quickwatch1

And there it is... look at those Id properties on the Documents.

Even though I am creating new instances of my in-memory db context for each test, the id numbering is not reset to zero.

Now I knew what the problem was, I Googled it with Bing and found the following:

At least I'm not the only one that has suffered from this!

Solution

There is actually more than one way to solve this problem. We can either change the way that our in-memory db context is created so that the numbering resets or change the way we're testing to negate the problem. I think the latter is probabaly the better way, but let's take a look at both.

Resetting the numbering

Initially, I only read the first of the 2 links above and focused on this comment:

As @rowanmiller said, if you want really independent tests, then you will need to create a new service provider for each test.

But, how? Here's how I refactored my test class to do it:

[TestClass]
public class UnitTest1 {

    private ServiceProvider _provider;

    public UnitTest1() {

        var services = new ServiceCollection();

        // Add the db context with a service scope of transient.
        // This will create a new instance everytime is requested.
        services.AddDbContext<DocumentDbContext>(
            options => options.UseInMemoryDatabase($"db-{Guid.NewGuid()}"),
            ServiceLifetime.Transient
            );

        _provider = services.BuildServiceProvider();

    }

    [DataTestMethod]
    [DataRow(1)]
    [DataRow(2)]
    public async Task Get_Document_By_Id_Returns_Result(int id) {

        // ARRANGE: create an in-memory instance of my db context
        var db = _provider.GetService<DocumentDbContext>();

        // ARRANGE: create my service class, passing in the db context
        var service = new DocumentService(
            db
            );

        // ARRANGE: create some test data
        db.Documents.AddRange(new[] {
            new Document{ Title = "Document 1"},
            new Document{ Title = "Document 2"}
        });
        await db.SaveChangesAsync();


        // ACT: get the document by Id
        var result = await service.GetById(id);

        // ASSERT
        Assert.IsNotNull(result);

    }

}

I added a constructor to setup DI with the db context in there. I used the service scope of Transient so that a new instance is created everytime it's requested.

This solved the problem. Alternatively, you could use the option in the second GitHub issue linked above. I haven't reviewed the merits of the one solution against the other, so it's up to you!

Refactor the test

This is probably the better option as it removes the dependency on the particular database provider that is used by Entity Framework Core.

For example, imagine if for some crazy reason the developers of the in-memory database provider decided that identity columns should only use odd numbers, so your ID sequence became 1, 3, 5, 7, 9, etc. Your application would likely still work, but your tests would fail.

The solution is to retrieve the ID as it appears in the database after the save and pass this to the service:

[TestClass]
public class UnitTest1 {

    [DataTestMethod]
    [DataRow(0)]
    [DataRow(1)]
    public async Task Get_Document_By_Id_Returns_Result(int index) {

        // ARRANGE: create an in-memory instance of my db context
        var db = new DocumentDbContext(
            new DbContextOptionsBuilder<DocumentDbContext>()
                .UseInMemoryDatabase($"db-{Guid.NewGuid()}")
                .Options
            );

        // ARRANGE: create my service class, passing in the db context
        var service = new DocumentService(
            db
            );

        // ARRANGE: create some test data
        var documents = new[] {
            new Document{ Title = "Document 1"},
            new Document{ Title = "Document 2"}
        };
        db.Documents.AddRange(documents);
        await db.SaveChangesAsync();

        // ARRANGE: get the document id for the index provided
        var documentId = documents[index].Id;

        // ACT: get the document by Id
        var result = await service.GetById(documentId);

        // ASSERT
        Assert.IsNotNull(result);

    }

}

Now we have a test that doesn't care about the number that the database provider uses and will always use a valid ID in the test. Problem solved.