Нужно было получить без отслеживания, но зачем? Тесты и приспособления не имеют общего контекста? Не так ли?
(решение под вопросом)
[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 (сбой такой же, как и в локальной среде)
Я пытался вызвать контекст БД из другого места, чтобы очистить что-либо постоянное, но ничего не получилось.
Проблема решена, но все равно хотелось бы объяснить, почему это происходит.
Решение состоит в том, чтобы добавить 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 пропустит сопоставление данных для уже отслеживаемых объектов (на основе первичного ключа, хотя он должен «отслеживать» добавления и удаления), отсюда и поведение, которое вы наблюдаете: тест обновления завершается неудачей, поскольку используются устаревшие данные.
У вас есть как минимум следующие варианты:
dbContext.ChangeTracker.Clear();
после SaveChangesAsync
в методах раздачи (например, CreateRandomUserInDb
или CreateRandomUsersInDb
)AsNoTracking
Возможно, еще несколько идей — Как работает кеширование в Entity Framework? см. примеры кода там.
@VyronPaschalidis, пожалуйста, посмотрите обновленный ответ. Предыдущий был неверным, xUnit здесь не является источником проблемы (он создаст новый экземпляр тестового класса для каждого теста, общим является только
IntegrationTestApiFactory
, а не клиент/контекст - неправильно прочитал документацию), а какой-то причудливый EF Core поведение, смог подтвердить, что второй вариант работает с вашим реальным кодом.