Search code examples
c#asp.net-web-apientity-framework-coreintegration-testingxunit.net

Injecting customized DbContext into Asp.NET controller during integration test


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.


Solution

  • 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();
            }
        }
    }
    
    
    

    Debug

    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