web-dev-qa-db-ja.com

Entity Framework Core 3.1とServiceProviderのUseInMemoryDatabaseオプションの使用(スコープの有効期間)

Webアプリケーションプロジェクトを.NET Core 2.1から3.1(EF Coreも2.1.1から3.1.0に)に移行しました。

移行後、一部の単体テストが機能しなくなり、重複キーのdb例外がスローされます。

私は問題をシミュレートし、オプションUseInMemoryDatabaseを使用したEFコアが3.1では動作が異なることに気付きました。古いデータはクリーンアップされません。

2番目のテスト方法では、Peopleテーブルに最初のテストから追加されたデータがすでに含まれていますが、これは2.1では発生しません

インメモリデータベースを各単体テストにスコープする方法を知っている人はいますか?

これが私のテストコードです:

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

各テストのサービスプロバイダーを個人的に構築するので、同時に実行されるテスト間で状態が共有されないようにします。このようなもの:

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

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

    return services.BuildServiceProvider();
}

次に、この関数を使用して、各テストでプロバイダーを構築します

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

これにより、実行時間が以前よりも少し長くなる可能性がありますが、現在の問題の再発を確実に防ぐことができます。

ヒント:

netcoreapp3.1で実行しているため、ステートメントを使用してc#8構文を使用することもできます。

[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

ユニットテストに使用するin-memory databaseを実行する場合は、Microsoftが提供するnugetパッケージを使用できます。

Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 3.1.5

また、DbContextとリポジトリを偽装できるように構成します。次に例を示します。

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

次に、このFakeDbContextを使用して、単体テストプロジェクトのFakeRepositoryに注入できます。

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

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

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

これで、メモリ内データベースを使用して単体テストを実行できるようになりました。例えば:

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