Контекст Testcontainers .NET 8 db, похоже, не обновляется в [Факт], проблема решена, ищет объяснение

Я пытаюсь реализовать интеграционный тест на конечной точке Put контроллера crud.


TL; доктор;

Нужно было получить без отслеживания, но зачем? Тесты и приспособления не имеют общего контекста? Не так ли?

(решение под вопросом)


[HttpPut("{id}")]
    public async Task<IActionResult> Put([FromRoute] string id, [FromBody] UpdateUser updateUser, [FromServices] CaManDbContext dbContext, CancellationToken cancellationToken)
    {
        if (!Ulid.TryParse(id, out var ulId))
        {
            return BadRequest();
        }
        
        var existingUser = await dbContext.Users
            .Include(u => u.ContactInfo)
            .FirstOrDefaultAsync(u => u.Id == new UserId(ulId), cancellationToken);

        if (existingUser is null)
        {
            return NotFound();
        }

        if (!string.IsNullOrWhiteSpace(updateUser.shortName))
        {
            var newShortName = ShortName.Create(updateUser.shortName);

            existingUser.UpdateShortName(newShortName);
        }

        if (!string.IsNullOrWhiteSpace(updateUser.email))
        {
            var newEmail = Email.Create(updateUser.email);

            existingUser.UpdateEmail(newEmail);
        }

        await dbContext.SaveChangesAsync(cancellationToken);

        return Ok(existingUser);
    }

Я уже реализовал приспособление API, и тесты на других конечных точках проходят гладко.


Однако следующий тест

[Fact]
    public async Task Update_ShouldUpdate_EmailOfExistingUser_ToDatabase()
    {
        // Arrange
        var existingUser = await UserHelperMethods.CreateRandomUserInDb(_apiDbContext);
        
        var shortName = existingUser.ShortName.Value;
        var email = "[email protected]";

        // Act
        var httpResponse =
            await _apiClient.PutAsJsonAsync($"/api/Users/{existingUser.Id.Value}", 
                new UpdateUser(null, email, null));

        Assert.True(httpResponse.IsSuccessStatusCode);
        
        var updatedUser = await httpResponse.Content.ReadFromJsonAsync<CreatedTestUser>();

        //Assert
        Assert.NotNull(updatedUser);
        Assert.Equal(existingUser.Id, updatedUser.Id);
        Assert.Equal(shortName, updatedUser.ShortName.Value);
        Assert.Equal(email, updatedUser.Email.Value);
        
        var fetchedUser = await (await _apiClient.GetAsync($"api/Users/{existingUser.Id.Value}")).Content.ReadFromJsonAsync<CreatedTestUser>();
        
        Assert.NotNull(fetchedUser);
        Assert.Equal(updatedUser.Id, fetchedUser.Id);
        Assert.Equal(updatedUser.ShortName.Value, fetchedUser.ShortName.Value);
        Assert.Equal(updatedUser.Email.Value, fetchedUser.Email.Value);

        var dbUser = await _apiDbContext.Users.FirstOrDefaultAsync(u => u.Id == updatedUser.Id);
        
        Assert.NotNull(dbUser);
        Assert.Equal(updatedUser.Id, dbUser.Id);
        Assert.Equal(updatedUser.ShortName.Value, dbUser.ShortName.Value);
        Assert.Equal(updatedUser.Email.Value, dbUser.Email.Value);
    }

не получается на последней строке Assert.Equal(updatedUser.Email.Value, dbUser.Email.Value);

Сообщение об ошибке указывает, что, хотя запрос обработан правильно, пользователь внутри контекста базы данных не обновляется!


Фабрика API:

public class IntegrationTestApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly MySqlContainer 
        _dbContainer = new MySqlBuilder()
            .WithImage("mysql:8.0")
            .Build();

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            var dbDescriptor = services
                .SingleOrDefault(s => s.ServiceType == typeof(DbContextOptions<CaManDbContext>));

            if (dbDescriptor is not null)
            {
                services.Remove(dbDescriptor);
            }

            services.AddDbContext<CaManDbContext>(optionsBuilder =>
            {
                var serverVersion = new MySqlServerVersion(new Version(8, 0, 36));
                optionsBuilder.UseMySql(_dbContainer.GetConnectionString(), serverVersion);
            });
        });
    }

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();
        using var scope = Services.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<CaManDbContext>();
        await dbContext.Database.MigrateAsync();
    }

    public new Task DisposeAsync()
    {
        return _dbContainer.StopAsync();
    }
}

и базовый класс интеграционного теста;

public abstract class BaseIntegrationTest : IClassFixture<IntegrationTestApiFactory>
{
    protected readonly HttpClient _apiClient;
    protected readonly CaManDbContext _apiDbContext;
    protected readonly IServiceScope _apiScope;
    
    protected BaseIntegrationTest(IntegrationTestApiFactory apiFactory)
    {
        _apiClient = apiFactory.Server.CreateClient();
        _apiScope = apiFactory.Services.CreateScope();
        _apiDbContext = _apiScope.ServiceProvider.GetRequiredService<CaManDbContext>();
    }
}

Весь код можно найти здесь , с помощью которого можно напрямую запускать тесты, а также здесь можно найти неудачный запуск теста по действию github (сбой такой же, как и в локальной среде)

Я пытался вызвать контекст БД из другого места, чтобы очистить что-либо постоянное, но ничего не получилось.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Проблема решена, но все равно хотелось бы объяснить, почему это происходит.

Решение состоит в том, чтобы добавить AsNotTracking() при получении обновленного пользователя, я думаю, чтобы избежать обращения к кэшированному объекту контекста с заданной областью.

Заменять

var dbUser = await _apiDbContext.Users.FirstOrDefaultAsync(u => u.Id == updateUser.Id);

с

var dbUser = await _apiDbContext.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == updateUser.Id);

Ответ принят как подходящий

Вы используете один и тот же экземпляр контекста для заполнения данных и проверки обновлений. EF Core и его операции DML используют отслеживание изменений (которое включено по умолчанию), поэтому, когда вы добавляете и сохраняете изменения, объекты присутствуют в средстве отслеживания изменений, и если вы будете запрашивать базу данных (с включенным отслеживанием изменений) EF пропустит сопоставление данных для уже отслеживаемых объектов (на основе первичного ключа, хотя он должен «отслеживать» добавления и удаления), отсюда и поведение, которое вы наблюдаете: тест обновления завершается неудачей, поскольку используются устаревшие данные.

У вас есть как минимум следующие варианты:

  1. Создайте различные области и контексты для заполнения и остальных манипуляций (потенциально лучший вариант, вы пытаетесь обернуть его в какой-нибудь вспомогательный метод, который будет принимать действие/функцию для выполнения над экземпляром контекста)
  2. Добавьте вызовы dbContext.ChangeTracker.Clear(); после SaveChangesAsync в методах раздачи (например, CreateRandomUserInDb или CreateRandomUsersInDb)
  3. Отключите отслеживание «проверок», как вы это делаете с AsNoTracking

Возможно, еще несколько идей — Как работает кеширование в Entity Framework? см. примеры кода там.

@VyronPaschalidis, пожалуйста, посмотрите обновленный ответ. Предыдущий был неверным, xUnit здесь не является источником проблемы (он создаст новый экземпляр тестового класса для каждого теста, общим является только IntegrationTestApiFactory, а не клиент/контекст - неправильно прочитал документацию), а какой-то причудливый EF Core поведение, смог подтвердить, что второй вариант работает с вашим реальным кодом.

Guru Stron 13.08.2024 22:40

Другие вопросы по теме