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>
各テストのサービスプロバイダーを個人的に構築するので、同時に実行されるテスト間で状態が共有されないようにします。このようなもの:
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);
}
ユニットテストに使用する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);
}
}
}