Как выполнить шаблоны UnitOfWork Repository с ядром entity Framework и модульным тестированием

#.net-core #entity-framework-core #xunit

#.net-core #entity-framework-core #xunit

Вопрос:

Я столкнулся с проблемой во время модульного тестирования, когда, если я запускаю несколько тестов одновременно, DbContext будут потеряны записи, которые я добавил во время модульных тестов, и я думаю, что это может быть связано с тем, как сервисы зарегистрированы в my ServiceCollection .

У меня следующая настройка:

IUnitOfWork:

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

        int Complete();
    }
 

UnitOfWork

     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.Пользователи в репозитории, но я вижу это в UnitOfWork.Пользователи в тесте.

Каков правильный подход здесь?

Редактировать 1: Пробовал некоторые другие изменения, но пока безуспешно. Настроено UnitOfWork так, чтобы использовать интерфейсы каждого репозитория и регистрировать их в BaseTest качестве служб с ограниченной областью действия. Также пытался пометить BaseTest как реализацию IDisposable , а затем выполнить:

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

На уровне сервиса я буду видеть пользователей просто отлично, но как только я перейду на уровень репозитория, я потеряю пользователей:/ У меня есть подозрение, что это связано с внедрением зависимостей AddScoped vs 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) , но проблема все еще сохранялась.

Это очень раздражает при настройке.

Ответ №1:

Это то, что решило это для меня на данный момент.

Резюме: Создан новый ServiceFixture . Это ServiceFixture применяется к BaseTest классу как IClassFixture<ServiceFixture> . ServiceFixture Отвечает за инициализацию коллекции сервисов и позволяет повторно использовать ее в разных тестовых классах. Цель BaseTest состоит в том, чтобы разрешить удаление базы данных и другую очистку, которая необходима после каждого теста. Dispose Метод этого класса отсоединит состояние объекта, а также удалит базу данных.

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

UserServiceTests.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);
        }
    }