首页 > 解决方案 > 如何使用实体框架核心和单元测试来做 UnitOfWork + Repository 模式

问题描述

我在单元测试时遇到了一个问题,如果我一次运行多个测试,DbContext将会丢失我在单元测试期间添加的记录,我认为这可能与服务在我的ServiceCollection.

我有以下设置:

工作单位:

    public interface IUnitOfWork : IDisposable
    {
        IUserRepository Users { get; }

        int Complete();
    }

工作单位

    public class UnitOfWork : IUnitOfWork
    {
        private readonly MyDbContext _context;

        public IUserRepository Users { get; }

        public UnitOfWork(MyDbContext context,
                          IUserRepository userRepository)
        {
            _context = context;

            Users = userRepository;
        }

        public void Dispose() => _context.Dispose();

        public int Complete() => _context.SaveChanges();
    }

用户存储库

    public class UserRepository : Repository<User>, IUserRepository
    {
        public UserRepository(MyDbContext context) : base(context) { }

        public MyDbContext MyDbContext => Context as MyDbContext;

        public Task<User?> GetUserDetailsAsync(int userID)
        {
            var user = MyDbContext.Users.Where(user => user.Id == userID)
                                                  .Include(user => user.Emails)
                                                  .Include(user => user.PhoneNumbers).FirstOrDefault();

            return Task.FromResult(user);
        }
    }

这是我的基本测试:

    public abstract class BaseTest : IDisposable
    {
        protected ServiceProvider ServiceProvider { get; }

        private MyDbContext MyDbContext { get; }

        protected IUnitOfWork UnitOfWork { get; }

        public BaseTest()
        {
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddScoped<IUserService, UserService>()
                             .AddScoped<IUnitOfWork, UnitOfWork>()
                             .AddScoped(typeof(IRepository<>), typeof(Repository<>))
                             .AddScoped<IOrganizationRepository, OrganizationRepository>()
                             .AddScoped<IExercisePostRepository, ExercisePostRepository>()
                             .AddScoped<IUserRepository, UserRepository>()
                             .AddTransient<IRestClient, RestClient>()
                             .AddAutoMapper(typeof(Startup).Assembly)
                             .AddDbContext<MyDbContext>(options =>
                                                                  options.UseInMemoryDatabase("Core")
                                                                         .EnableSensitiveDataLogging());
            ServiceProvider = serviceCollection.BuildServiceProvider();
            SpotcheckrCoreContext = ServiceProvider.GetRequiredService<MyDbContext>();
            MyDbContext.Database.EnsureCreated();
            UnitOfWork = ServiceProvider.GetRequiredService<IUnitOfWork>();
        }

        public void Dispose()
        {
            MyDbContext.Database.EnsureDeleted();
            UnitOfWork.Dispose();
        }
    }

样品测试:

    public class UserServiceTests : BaseTest
    {
        private readonly IUnitOfWork UnitOfWork;

        private readonly IUserService Service;

        public UserServiceTests()
        {
            UnitOfWork = ServiceProvider.GetRequiredService<IUnitOfWork>();
            Service = ServiceProvider.GetRequiredService<IUserService>();
        }

        [Fact]
        public async void GetUserAsync_WithValidUser_ReturnsUser()
        {
            var user = new User
            {
                FirstName = "John",
                LastName = "Doe"
            };
            UnitOfWork.Users.Add(user);
            UnitOfWork.Complete();

            var result = await Service.GetUserAsync(user.Id);
            Assert.Equal(user.Id, result.Id);
        }
}

如果我自己运行这个测试,那么它将正确通过,我可以在存储库中看到用户。但是,如果我使用其他测试和调试运行它,那么一旦我在存储库中检查 UnitOfWork.Users,该用户就会丢失,但我确实在测试中的 UnitOfWork.Users 中看到它。

这里的正确方法是什么?

编辑 1:尝试了其他一些更改,但还没有运气。调整UnitOfWork为接收每个存储库的接口并将它们注册BaseTest为范围服务。还尝试标记BaseTest为实施IDisposable然后执行:

        public void Dispose()
        {
            MyDbContext.Database.EnsureDeleted();
            UnitOfWork.Dispose();
        }

在服务层中,我会看到用户很好,但是一旦我进入存储库层,我就会失去用户:/我怀疑它与依赖注入有关,AddScoped以及AddTransient所有这些如何与运行多个单元测试。

编辑2:尝试了更多的东西......IClassFixture<BaseTest>在每个测试类上使用,然后确保每个测试类都实现IDisposable了,在那里我将确保删除上下文数据库;还确保在测试类构造函数中创建了它。有了这个我结束了以下错误:

The instance of entity type cannot be tracked because another instance with the same key value for {'Id'} is already being tracked

所以我补充说.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking),但问题仍然存在。

这设置起来很烦人。

标签: .net-coreentity-framework-corexunit

解决方案


这就是现在为我解决的问题。

摘要:创建了一个新的ServiceFixture. 这ServiceFixture适用于BaseTestIClassFixture<ServiceFixture>ServiceFixture负责初始化服务集合并允许在不同的测试类中重用它。的目的BaseTest是允许在每次测试后处理数据库和其他必要的清理。Dispose该类的方法将分离实体状态并删除数据库。

服务夹具.cs

public class ServiceFixture
    {
        public ServiceProvider ServiceProvider { get; }

        public ServiceFixture()
        {
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddScoped<IUserService, UserService>()
                             .AddScoped<ICertificationService, CertificationService>()
                             .AddScoped<IOrganizationService, OrganizationService>()
                             .AddScoped<ICertificateService, CertificateService>()
                             .AddScoped<IUnitOfWork, UnitOfWork>()
                             .AddScoped<IUserRepository, UserRepository>()
                             .AddScoped<IExercisePostRepository, ExercisePostRepository>()
                             .AddScoped<IEmailRepository, EmailRepository>()
                             .AddScoped<IPhoneNumberRepository, PhoneNumberRepository>()
                             .AddScoped<ICertificationRepository, CertificationRepository>()
                             .AddScoped<ICertificateRepository, CertificateRepository>()
                             .AddScoped<IOrganizationRepository, OrganizationRepository>()
                             .AddTransient<IRestClient, RestClient>()
                             .AddSingleton<NASMCertificationValidator>()
                             .AddAutoMapper(typeof(Startup).Assembly)
                             .AddDbContext<SpotcheckrCoreContext>(options =>
                                                                  options.UseInMemoryDatabase("Spotcheckr-Core")
                                                                         .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
                                                                         .EnableSensitiveDataLogging());
            ServiceProvider = serviceCollection.BuildServiceProvider();
        }
    }

BaseTest.cs

    public abstract class BaseTest : IClassFixture<ServiceFixture>, IDisposable
    {
        protected readonly ServiceProvider ServiceProvider;

        protected readonly IUnitOfWork UnitOfWork;

        private readonly SpotcheckrCoreContext Context;

        public BaseTest(ServiceFixture serviceFixture)
        {
            ServiceProvider = serviceFixture.ServiceProvider;
            Context = serviceFixture.ServiceProvider.GetRequiredService<SpotcheckrCoreContext>();
            UnitOfWork = serviceFixture.ServiceProvider.GetRequiredService<IUnitOfWork>();

            Context.Database.EnsureCreated();
        }

        public void Dispose()
        {
            Context.ChangeTracker.Entries().ToList().ForEach(entry => entry.State = EntityState.Detached);
            Context.Database.EnsureDeleted();
        }
    }

用户服务测试.cs

    public class UserServiceTests : BaseTest
    {
        private readonly IUserService Service;

        public UserServiceTests(ServiceFixture serviceFixture) : base(serviceFixture)
        {
            Service = serviceFixture.ServiceProvider.GetRequiredService<IUserService>();
        }

        [Fact]
        public async void GetUserAsync_WithInvalidUser_ThrowsException()
        {
            Assert.ThrowsAsync<InvalidOperationException>(() => Service.GetUserAsync(-1));
        }

        [Fact]
        public void CreateUser_UserTypeAthlete_CreatesAthleteUser()
        {
            var result = Service.CreateUser(Models.UserType.Athlete);
            Assert.IsType<Athlete>(result);
        }

        [Fact]
        public void CreateUser_UserTypePersonalTrainer_CreatesPersonalTrainerUser()
        {
            var result = Service.CreateUser(Models.UserType.PersonalTrainer);
            Assert.IsType<PersonalTrainer>(result);
        }
    }

推荐阅读