14

I have migrated a web application project from .NET Core 2.1 to 3.1 (also EF Core from 2.1.1 to 3.1.0).

After the migration, some unit tests are not working anymore, throwing duplicate keys db exception.

I simulated the problem and realize that EF core with option UseInMemoryDatabase is behaving differently in 3.1, it does not clean up the old data.

In the second test method, the People table already contains data added from the first test, which is not happening in 2.1

Does anyone know how can I make in-memory database to be scoped to each unit test?

Here is my testing code:

AppDbContext.cs

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;

namespace MyConsoleApp.Database
{
    public class AppDbContext: DbContext
    {
        protected AppDbContext(DbContextOptions options) : base(options) { }

        public AppDbContext(DbContextOptions<AppDbContext> options) : this((DbContextOptions)options)
        {
        }

        public virtual DbSet<Person> Person { get; set; }
    }

    public class Person
    {
        [Key]
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

AppUnitTest.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyConsoleApp.Database;
using System.Linq;

namespace MyConsoleAppTest
{
    [TestClass]
    public class AppUnitTest
    {
        public ServiceCollection Services { get; private set; }
        public ServiceProvider ServiceProvider { get; protected set; }

        [TestInitialize]
        public void Initialize()
        {
           Services = new ServiceCollection();

           Services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase(databaseName: "InMemoryDb"), 
               ServiceLifetime.Scoped, 
               ServiceLifetime.Scoped);

            ServiceProvider = Services.BuildServiceProvider();
        }

        [TestMethod]
        public void TestMethod1()
        {
            using (var dbContext = ServiceProvider.GetService<AppDbContext>())
            {
                dbContext.Person.Add(new Person { Id = 0, Name = "test1" });
                dbContext.SaveChanges();
                Assert.IsTrue(dbContext.Person.Count() == 1);
            }
        }

        [TestMethod]
        public void TestMethod2()
        {
            using (var dbContext = ServiceProvider.GetService<AppDbContext>())
            {
                dbContext.Person.Add(new Person { Id = 0, Name = "test2" });
                dbContext.SaveChanges();
                Assert.IsTrue(dbContext.Person.Count() == 1);
            }
        }

        [TestCleanup]
        public virtual void Cleanup()
        {
            ServiceProvider.Dispose();
            ServiceProvider = null;
        }
    }
}

MyConsoleAppTest.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
    <PackageReference Include="MSTest.TestAdapter" Version="2.0.0" />
    <PackageReference Include="MSTest.TestFramework" Version="2.0.0" />
    <PackageReference Include="coverlet.collector" Version="1.0.1" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyConsoleApp\MyConsoleApp.csproj" />
  </ItemGroup>

</Project>
marc_s
  • 675,133
  • 158
  • 1,253
  • 1,388
James
  • 421
  • 1
  • 5
  • 20
  • 1
    Create a scope in each test and resolve the service using the scope, ie `using(var scope=ServiceProvider,CreateScope){var ctx=scope.GetService(...)`. Exiting the scope will dispose the scoped/transient objects created by it – Panagiotis Kanavos Jan 08 '20 at 18:08

5 Answers5

29

You can install the package via package console

Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 3.1.5

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory

Gert Arnold
  • 93,904
  • 24
  • 179
  • 256
rizu
  • 776
  • 1
  • 7
  • 19
  • 3
    How is this related to the question, which is how to scope an in-memory database to one test? – Gert Arnold Jul 07 '20 at 19:25
  • 1
    @GertArnold , it is not related, I see this many times, some peoples are just throwing an answer and then is asking you to flag it as the correct answer, without adding any other explanation to the solution. I think they are just hunting for reputation score points, perhaps some companies use this info when are hiring. – James Jul 10 '20 at 13:51
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/low-quality-posts/26642255) – Alex Mendez Jul 10 '20 at 16:23
  • 3
    I was just looking for this. Thanks sir. – Manguera v Oct 01 '20 at 15:43
10

The solution for me was to change the database name with unique name.

Services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()), ServiceLifetime.Scoped, ServiceLifetime.Scoped);

In this way there is a new database for each test method.

See the github issue: https://github.com/dotnet/efcore/issues/19541

James
  • 421
  • 1
  • 5
  • 20
1

I made the following Extension method for my UnitTest:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddInMemoryDbContext<TDbContext>(this IServiceCollection services) where TDbContext: DbContext
        => services.AddDbContext<TDbContext>(builder
            => builder.UseInMemoryDatabase(Guid.NewGuid().ToString())
                .ConfigureWarnings(w =>
                    {
                        w.Ignore(InMemoryEventId.TransactionIgnoredWarning);
                        w.Throw(RelationalEventId.QueryClientEvaluationWarning);
                    }
                ), ServiceLifetime.Transient);
}

The usage is very simple. Just add all of your DbContext to your IServiceCollection on UnitTest setup.

...

services
    .AddInMemoryDbContext<AppDbContext>()
    .AddInMemoryDbContext<FooDbContext>()
    .AddInMemoryDbContext<AnotherDbContext>();

...

You also need to install Microsoft.EntityFrameworkCore.InMemory (https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory)

Dominic Jonas
  • 3,671
  • 1
  • 25
  • 55
0

I would personally build a service-provider for each test so you'll make sure that there is no shared-state between tests that are being executed simultaneously. Something like this:

private IServiceProvider BuildServiceProvider()
{
    var services = new ServiceCollection();

    services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase(databaseName: "InMemoryDb"), 
        ServiceLifetime.Scoped, 
        ServiceLifetime.Scoped);

    return services.BuildServiceProvider();
}

Then use this function to build the provider in each test

[TestMethod]
public void TestMethod1()
{
    using (var serviceProvider = BuildServiceProvider()) 
    {
        using (var dbContext = serviceProvider.GetService<AppDbContext>())
        {
            dbContext.Person.Add(new Person { Id = 0, Name = "test1" });
            dbContext.SaveChanges();
            Assert.IsTrue(dbContext.Person.Count() == 1);
        }
    }
}

This might cause the execution time to be a little bit higher than before but should definitely prevent your current problem from happening again.

Tip:

You could also use the c# 8 syntax using statements now since you are running on netcoreapp3.1

[TestMethod]
public void TestMethod1()
{
    using var serviceProvider = BuildServiceProvider();

    using var dbContext = ServiceProvider.GetService<AppDbContext>();

    dbContext.Person.Add(new Person { Id = 0, Name = "test1" });

    dbContext.SaveChanges();

    Assert.IsTrue(dbContext.Person.Count() == 1);
}
alsami
  • 6,371
  • 3
  • 16
  • 29
  • Thanks @alsami, I will try but I don't understand why in EF core 2.1 is working. For each unit test method, Initialize and Cleanup are called, so moving the service provider code in each method should behave in the same way. – James Jan 09 '20 at 05:45
0

If you want to do in-memory database to be used for unit testing, you can use the nuget package that microsoft provides for it:

Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 3.1.5

And also to configure that you can fake the DbContext and the repositories, for example:

namespace YourProject.Tests.UnitTests
{
    public class FakeDbContext : DbContext
    {
        public DbSet<Entity> Entities { get; set; }    

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseInMemoryDatabase(databaseName: "FakePersonalSiteDbContext");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            AddMappingOverrides(modelBuilder);
        }

        private void AddMappingOverrides(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new SomeEntityMappingOverride());
        }
    }
}

And then u can use this FakeDbContext to inject to a FakeRepository in unit testing project:

namespace YourProject.Tests.UnitTests
{
    public class FakePersonalSiteRepository : IYourRepository
    {
        private FakeDbContext dbContext;

        public FakeRepository(FakeDbContext dbContext)
        {
            this.dbContext = dbContext;
        }

        // Your repository methods (Add, Delete, Get, ...)
    }
}

That way now you can use a in-memory database to your unit tests. For example:

namespace YourProject.Tests.UnitTests
{
    public class UnitTestBase
    {
        protected IYourRepositoryRepository Repository { get; set; }
        protected FakeDbContext FakeDbContext { get; set; }

        [SetUp]
        public void SetUp()
        {
            FakeDbContext = new FakeDbContext();
            FakeDbContext.Database.EnsureDeleted();
            Repository = new FakeRepository(FakeDbContext);
        }
    }
}
pablocom96
  • 59
  • 5