I’m trying to implement an integration test on the Put endpoint of a crud controller
tl;dr;
Needed to fetch without tracking, but why? Tests and fixture don’t share context? Shouldn’t they?
[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 = "[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);
}
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
Solved the issue but still would like an explanation of why it’s happening.
The solution is to add AsNotTracking() while fetching the updated user, I guess to avoid reaching to the cached entity of the scoped context.
Replace
var dbUser = await _apiDbContext.Users.FirstOrDefaultAsync(u => u.Id == updatedUser.Id);
with
var dbUser = await _apiDbContext.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == updatedUser.Id);