web-dev-qa-db-fra.com

Utilisation d'Entity Framework Core 3.1 avec l'option UseInMemoryDatabase dans ServiceProvider (durée de vie limitée)

J'ai migré un projet d'application Web de .NET Core 2.1 vers 3.1 (également EF Core de 2.1.1 vers 3.1.0).

Après la migration, certains tests unitaires ne fonctionnent plus, lançant une exception db de clés en double.

J'ai simulé le problème et je me rends compte que EF core avec l'option UseInMemoryDatabase se comporte différemment dans 3.1, il ne nettoie pas les anciennes données.

Dans la deuxième méthode de test, la table People contient déjà des données ajoutées à partir du premier test, ce qui ne se produit pas dans la version 2.1

Quelqu'un sait-il comment puis-je faire en sorte que la base de données en mémoire soit étendue à chaque test unitaire?

Voici mon code de test:

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>
6
James

Je créerais personnellement un fournisseur de services pour chaque test afin que vous vous assuriez qu'il n'y a pas d'état partagé entre les tests qui sont exécutés simultanément. Quelque chose comme ça:

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

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

    return services.BuildServiceProvider();
}

Ensuite, utilisez cette fonction pour créer le fournisseur dans chaque 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);
        }
    }
}

Cela peut entraîner un temps d'exécution un peu plus élevé qu'avant, mais cela devrait certainement empêcher votre problème actuel de se reproduire.

Pointe:

Vous pouvez également utiliser la syntaxe c # 8 en utilisant des instructions maintenant puisque vous exécutez sur 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);
}
0
alsami

Si vous voulez faire base de données en mémoire à utiliser pour les tests unitaires, vous pouvez utiliser le package nuget que Microsoft fournit pour cela:

Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 3.1.5

Et aussi pour configurer que vous pouvez simuler le DbContext et les référentiels, par exemple:

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

Et puis vous pouvez utiliser ce FakeDbContext pour injecter dans un FakeRepository dans un projet de test unitaire:

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

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

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

De cette façon, vous pouvez maintenant utiliser une base de données en mémoire pour vos tests unitaires. Par exemple:

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);
        }
    }
}
0
pablocom96