I have the following simplified ASP.net API controller to test:
[Route("api")]
public class CustomerController(Func<DataContext> factory) : Controller
{
[HttpPost("customer")]
public async Task<IActionResult> Create([FromBody] CustomerRequest request)
{
using var context = factory.Invoke();
var uow = new UnitOfWork(context);
await uow.CustomerService.Create(request.ToCustomer());
return Ok();
}
}
And below is my test:
[Collection(nameof(ControllerTestCollection))]
public class CustomerApiTest(IntegrationTestFactory factory)
{
private readonly HttpClient client = factory.CreateClient();
[Fact]
public async Task TestCreateCustomer()
{
var response = await client.PostAsJsonAsync("api/customer", new CustomerRequest(1, "Customer 1"));
response.StatusCode.Should().Be(HttpStatusCode.Created);
}
}
And below is my customized WebApplicationFactory:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices
(
services =>
{
services.RemoveDbContext<DataContext>();
services.AddDbContext<DataContext>(options => options.UseMySql(testcontainer.GetConnectionString()));
services.EnsureDbCreated<DataContext>();
}
);
}
When I run the test, the controller keeps using the real DB setting instead of the one I setup for this test, so basically it ignores the DataContext pointing to DB hosted by Testcontainers. But I can assure that the customized WebApplicationFactory passed to the test correctly points to test DB.
I'm new to this ASP.Net & EF Core so any help would be greatly appreciated.
WebApplicationFactory<TEntryPoint>
is used to create a TestServer for the integration tests. TEntryPoint is the entry point class of the SUT, usually Program.cs.
**You are injecting deletgate Func<DataContext>
in controller's constructor. DI container has to know how to construct Func so we need to add it explicitly
builder.Services.AddTransient<Func<DataContext>>(cont => () => cont.GetService<DataContext>()!);
**
Runs fine, passes DataContext
correctly
Program.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using System.Text.Json.Serialization;
var builder = Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(args);
builder
.Services
.AddControllers(options => options.SuppressAsyncSuffixInActionNames = false)
.AddJsonOptions(options => options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddDbContext<DataContext>(options => options.UseInMemoryDatabase("TestApp"));
builder.Services.AddTransient<Func<DataContext>>(cont => () => cont.GetService<DataContext>()!);
builder.Services.AddAuthorization();
builder.Services
.AddIdentityApiEndpoints<IdentityUser>()
.AddEntityFrameworkStores<DataContext>();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.MapControllers();
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
);
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapIdentityApi<IdentityUser>();
app.Run();
public partial class Program { }
CustomWebApplicationFactory
using System.Data.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<DataContext>));
services.Remove(dbContextDescriptor!);
var dbConnectionDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbConnection));
services.Remove(dbConnectionDescriptor!);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<DataContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
services.AddTransient<Func<DataContext>>(cont => () => cont.GetService<DataContext>()!);
});
builder.UseEnvironment("Development");
}
}
Test
public class CustomerApiTest(CustomWebApplicationFactory<Program> factory) : **IClassFixture<CustomWebApplicationFactory<Program>>**
{
private readonly HttpClient client = factory.CreateClient();
[Fact]
public async Task TestCreateCustomer()
{
var response = await client.GetAsync("/WeatherForecast");
}
}
DB Context
using Microsoft.EntityFrameworkCore;
public class DataContext(DbContextOptions<DataContext> options) : DbContext(options)
{
public virtual DbSet<Message> Messages { get; set; }
}
Controller
using Microsoft.AspNetCore.Mvc;
namespace WebApplication.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController(ILogger<WeatherForecastController> logger, Func<DataContext> factory) : ControllerBase
{
private static readonly string[] Summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
private readonly ILogger<WeatherForecastController> _logger = logger;
private readonly DataContext context = factory.Invoke();
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
var messages = context.Messages.ToList();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}
Microsoft Documentation: https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-8.0#customize-webapplicationfactory
class-fixture: https://xunit.net/docs/shared-context#class-fixture