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