Repository navigation
Description
First of all, thanks for this package, looks very promising!
Problem
I want to test my ASP.NET Core application using WebApplicationFactory, similar to this example: https://thomhurst.github.io/TUnit/docs/examples/aspnet
That works. Then I want to inject dependencies (data factories) in my Test classes to easily create data. I know I can do that using ClassDataSource, but this only allows parameterless constructors. As soon as I add parameters (in my case, dependencies) the build fails with "must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'T' in the generic class".
Let's take this Factory as an example: It uses the AppDbContext that is used and registered throughout the API that is tested.
public class TestFactory
{
private readonly AppDbContext _dbContext;
public TestFactory(AppDbContext dbContext)
{
_dbContext = dbContext;
}
// Methods that use the DbContext
}As far as I understand, that issue can be solved using Class Constructor Helpers to allow creation of classes with parameters. When I take this as an example the unit test starts correctly.
But the test fails at runtime, because the ServiceCollection from the DiClassConstructor and the ServiceCollection from the regular API are not the same. The TestFactory receives a DbContext, but it's not the same DbContext that is used by the WebApplicationFactory, so changes made through one aren't visible to the other. Am I missing something here? Having all dependencies of the API available would be awesome, but I am not sure if it's possible at all..
Reproduction
// MinimalApi/Program.cs
using Microsoft.EntityFrameworkCore;
using MinimalApi.Data;
var builder = WebApplication.CreateBuilder(args);
// Add DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
// Enable testing
public partial class Program {}
// MinimalApi/Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
namespace MinimalApi.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<User> Users => Set<User>();
}
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
// TestProject/DataServices/TestFactory.cs
using MinimalApi.Data;
namespace TestProject.DataServices;
public class TestFactory
{
private readonly AppDbContext _dbContext;
public TestFactory(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task CreateUserAsync(string name)
{
_dbContext.Users.Add(new User { Name = name });
await _dbContext.SaveChangesAsync();
}
public async Task<IEnumerable<User>> GetAllUsersAsync()
{
return await _dbContext.Users.ToListAsync();
}
}
// TestProject/AppFactory.cs
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using MinimalApi;
using MinimalApi.Data;
using TUnit.Core.Interfaces;
namespace TestProject;
public class AppFactory : IAsyncInitializer, IAsyncDisposable
{
public WebApplicationFactory<Program>? Factory { get; private set; }
public async Task InitializeAsync()
{
Factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// test config
});
});
await Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
if (Factory != null)
{
await Factory.DisposeAsync();
}
}
}
// TestProject/DiClassConstructor.cs
using Microsoft.Extensions.DependencyInjection;
using TestProject.DataServices;
using TUnit.Core;
using TUnit.Core.Interfaces;
namespace TestProject;
public class DiClassConstructor : IClassConstructor, ITestEndEventReceiver
{
private static readonly IServiceProvider _serviceProvider;
private AsyncServiceScope _scope;
static DiClassConstructor()
{
var services = new ServiceCollection();
// Register TestFactory which depends on AppDbContext
services.AddTransient<TestFactory>();
_serviceProvider = services.BuildServiceProvider();
}
public T Create<T>(ClassConstructorMetadata classConstructorMetadata) where T : class
{
_scope = _serviceProvider.CreateAsyncScope();
return ActivatorUtilities.CreateInstance<T>(_scope.ServiceProvider);
}
public ValueTask OnTestEnd(TestContext testContext)
{
return _scope.DisposeAsync();
}
}
// TestProject/UserTests.cs
using Microsoft.AspNetCore.Mvc.Testing;
using MinimalApi;
using TestProject.DataServices;
using TUnit.Core;
namespace TestProject;
[ClassConstructor<DiClassConstructor>]
public class UserTests
{
private readonly TestFactory _testFactory;
public UserTests(TestFactory testFactory)
{
_testFactory = testFactory;
}
[Test]
[ClassDataSource<AppFactory>(Shared = SharedType.PerTestSession)]
public async Task CreateUser_ShouldAddUserToDatabase(AppFactory appFactory)
{
// This test will fail because TestFactory has no valid AppDbContext
// The AppDbContext is from the AppFactory but DiClassConstructor doesn't know about it
await _testFactory.CreateUserAsync("Test User");
// We'd need to get a new TestFactory instance with the correct DbContext
// But there's no clean way to do this with the current TUnit architecture
var users = await _testFactory.GetAllUsersAsync();
// This assertion will fail because TestFactory has a different AppDbContext
Assert.AreEqual(1, users.Count());
}
}
Activity
thomhurst commentedon Mar 12, 2025
So you essentially want to inject in the same
AppDbContextthat is in your WebFactory, into your tests too, so that you can manipulate it?BramMusters commentedon Mar 13, 2025
Yes. Not for manipulation but for full integration testing. I want to inject other dependencies as well. For example:
Whether I replace the dependency with a mock or anything else is up to the
AppFactorythomhurst commentedon Mar 13, 2025
Can you play about with something like this?
Make sure you're on v0.18.21
thomhurst commentedon Mar 17, 2025
@BramMusters ?
BramMusters commentedon Mar 18, 2025
Excuse my late response!
The build fails (0.18.40, .NET 8), with these errors:
Cannot implicitly convert type 'object[]' to X.DbContextCannot implicitly convert type 'object[]' to X.Firebase.IFirebaseClientthomhurst commentedon Mar 18, 2025
Can you try v0.18.45
thomhurst commentedon Mar 24, 2025
@BramMusters
BramMusters commentedon Mar 25, 2025
Sorry, you are just too fast;) . Yes, that works for the provided code, thanks!
However, in my very specific use case it does not work yet. I am not sure if it is even possible to support, but here is the situation anyway.
Shared = SharedType.None). That seems to work.configureWebHostso that it connects to the specific DB.testNameis used for the connectionString. However it isnull. Also, I noticed thatConfigureWebHostis called 3 times, I guess it should only be called once?Once again, I am not sure if this approach will work at all, but just to let you know :)
thomhurst commentedon Mar 28, 2025
Test name seems to work for me:
Here's the code:
Implementing
IAsyncInitializerand accessing theServerproperty will force the server to initialize. This will be done by one thread, and so should prevent multiple threads trying to do it, which I think might be why you're seeing it hit 3 times.github-actions commentedon Apr 28, 2025
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.
github-actions commentedon May 4, 2025
This issue was closed because it has been stalled for 5 days with no activity.