Needed to fetch without tracking, but why? Tests and fixture don't share context? Shouldn't they?
(solution under question)
[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);
}
I've already implemented the api fixture and tests on other endpoints run smoothly.
However, the following test
[Fact]
public async Task Update_ShouldUpdate_EmailOfExistingUser_ToDatabase()
{
// Arrange
var existingUser = await UserHelperMethods.CreateRandomUserInDb(_apiDbContext);
var shortName = existingUser.ShortName.Value;
var email = "test@test.com";
// 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);
}
fails on the last line Assert.Equal(updatedUser.Email.Value, dbUser.Email.Value);
The failure message indicates that even though the request is processed correctly, the user inside the db context is not updated!
The api factory:
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();
}
}
and the integration test base class;
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>();
}
}
The whole code can be found here which can directly run the tests, also here can be found the failed test run on github's action (the failure is the same as in local environment)
I tried calling the db context from different location in order to clean up anything persistent but got nowhere
You are using the same context instance to seed the data and to check for the updates. EF Core uses change tracking (is enabled by default for queries, most DML operations are done via tracking), so when you have added and saved changes, the entities are present in the change tracker and if you will query the database (with change tracking enabled) EF will skip the mapping of the data for already tracked entities (based on the primary key, though it should "monitor" additions and deletions), hence the behavior you observe - the update test fails since the stale data is used.
You have at least the following options:
dbContext.ChangeTracker.Clear();
calls after SaveChangesAsync
in the seeding methods (like CreateRandomUserInDb
or CreateRandomUsersInDb
)AsNoTracking
Potentially some more insights - How does caching work in Entity Framework? see code examples there.