diff --git a/CLAUDE.md b/CLAUDE.md
index 1ed9227dd1..925f3571f6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -147,6 +147,17 @@ dotnet test TUnit.Core.SourceGenerator.Tests
**Rule**: Only run TUnit.TestProject with explicit `--treenode-filter` to target specific tests or classes.
+**IMPORTANT: Run filters ONE AT A TIME!** Using OR patterns (`Pattern1|Pattern2`) can match thousands of unintended tests. Always run one specific filter per command:
+
+```bash
+# ❌ WRONG - OR patterns can match too broadly
+--treenode-filter "/*/*/ClassA/*|/*/*/ClassB/*"
+
+# ✅ CORRECT - Run separate commands for each class
+dotnet run -- --treenode-filter "/*/*/ClassA/*"
+dotnet run -- --treenode-filter "/*/*/ClassB/*"
+```
+
---
### Most Common Commands
diff --git a/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs b/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs
index f2389fda28..a4591d65b4 100644
--- a/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs
+++ b/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs
@@ -476,6 +476,12 @@ private static void TraverseInitializerProperties(
/// Collects root-level objects (class args, method args, properties) from test details.
/// Eliminates duplicate loops in DiscoverObjectGraph and DiscoverAndTrackObjects.
///
+ ///
+ /// For injected properties, only DIRECT test class properties (including inherited) are added at depth 0.
+ /// Nested properties (properties of injected objects) are discovered through normal
+ /// graph traversal at appropriate depths (1+), ensuring correct initialization order
+ /// for nested IAsyncInitializer dependencies. See GitHub issue #4032.
+ ///
private static void CollectRootObjects(
TestDetails testDetails,
TryAddObjectFunc tryAdd,
@@ -488,8 +494,70 @@ private static void CollectRootObjects(
// Process method arguments
ProcessRootCollection(testDetails.TestMethodArguments, tryAdd, onRootObjectAdded, cancellationToken);
- // Process injected property values
- ProcessRootCollection(testDetails.TestClassInjectedPropertyArguments.Values, tryAdd, onRootObjectAdded, cancellationToken);
+ // Build set of types in the test class hierarchy (for identifying direct properties)
+ var hierarchyTypes = GetTypeHierarchy(testDetails.ClassType);
+
+ // Process ONLY direct test class injected properties at depth 0.
+ // Nested properties will be discovered through normal graph traversal at depth 1+.
+ // This ensures proper initialization order for nested IAsyncInitializer dependencies.
+ foreach (var kvp in testDetails.TestClassInjectedPropertyArguments)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (kvp.Value == null)
+ {
+ continue;
+ }
+
+ // Check if this property belongs to the test class hierarchy (not nested object properties)
+ // Cache key format: "{DeclaringType.FullName}.{PropertyName}"
+ if (IsDirectProperty(kvp.Key, hierarchyTypes))
+ {
+ if (tryAdd(kvp.Value, 0))
+ {
+ onRootObjectAdded(kvp.Value);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets all types in the inheritance hierarchy from the given type up to (but not including) object.
+ ///
+ private static HashSet GetTypeHierarchy(Type type)
+ {
+ var result = new HashSet();
+ var currentType = type;
+
+ while (currentType != null && currentType != typeof(object))
+ {
+ if (currentType.FullName != null)
+ {
+ result.Add(currentType.FullName);
+ }
+
+ currentType = currentType.BaseType;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Determines if a cache key represents a direct property (belonging to test class hierarchy)
+ /// vs a nested property (belonging to an injected object).
+ /// Cache key format: "{DeclaringType.FullName}.{PropertyName}"
+ ///
+ private static bool IsDirectProperty(string cacheKey, HashSet hierarchyTypes)
+ {
+ // Find the last dot to separate type from property name
+ var lastDotIndex = cacheKey.LastIndexOf('.');
+ if (lastDotIndex <= 0)
+ {
+ return true; // Malformed key, treat as direct
+ }
+
+ var declaringTypeName = cacheKey.Substring(0, lastDotIndex);
+ return hierarchyTypes.Contains(declaringTypeName);
}
///
diff --git a/TUnit.TestProject/Bugs/4032/NestedAsyncInitializerTests.cs b/TUnit.TestProject/Bugs/4032/NestedAsyncInitializerTests.cs
new file mode 100644
index 0000000000..f04d018ba2
--- /dev/null
+++ b/TUnit.TestProject/Bugs/4032/NestedAsyncInitializerTests.cs
@@ -0,0 +1,417 @@
+using TUnit.Core.Interfaces;
+using TUnit.TestProject.Attributes;
+
+namespace TUnit.TestProject.Bugs._4032;
+
+///
+/// Regression tests for issue #4032: IAsyncInitializer lifecycle change broke complex test infrastructure orchestration.
+/// The problem was that nested IAsyncInitializer dependencies weren't initialized before the parent's InitializeAsync was called.
+/// This broke patterns like WebApplicationFactory + Testcontainers where the container must be initialized
+/// before the WebApplicationFactory tries to access the container's connection string.
+///
+
+#region Mock Classes
+
+///
+/// Simulates a Testcontainers PostgreSQL container.
+/// Must be initialized before any parent object tries to access GetConnectionString().
+///
+public class MockDatabaseContainer : IAsyncInitializer
+{
+ private static int _instanceCount;
+ private static int _initializeCount;
+ private static readonly List _initializationOrder = [];
+
+ public static int InstanceCount => _instanceCount;
+ public static int InitializeCount => _initializeCount;
+ public static IReadOnlyList InitializationOrder => _initializationOrder;
+
+ public int InstanceId { get; }
+ public bool IsInitialized { get; private set; }
+ public string? ConnectionString { get; private set; }
+
+ public MockDatabaseContainer()
+ {
+ InstanceId = Interlocked.Increment(ref _instanceCount);
+ }
+
+ public Task InitializeAsync()
+ {
+ Interlocked.Increment(ref _initializeCount);
+ IsInitialized = true;
+ ConnectionString = $"Host=localhost;Database=test_{InstanceId};User=test;Password=test";
+
+ lock (_initializationOrder)
+ {
+ _initializationOrder.Add(InstanceId);
+ }
+
+ Console.WriteLine($"[MockDatabaseContainer #{InstanceId}] InitializeAsync called - ConnectionString: {ConnectionString}");
+ return Task.CompletedTask;
+ }
+
+ public string GetConnectionString()
+ {
+ if (!IsInitialized || ConnectionString == null)
+ {
+ throw new InvalidOperationException(
+ $"MockDatabaseContainer #{InstanceId} is not initialized! " +
+ "This indicates a bug in the IAsyncInitializer lifecycle ordering. " +
+ "Nested dependencies must be initialized before parent objects.");
+ }
+
+ return ConnectionString;
+ }
+
+ public static void Reset()
+ {
+ _instanceCount = 0;
+ _initializeCount = 0;
+ lock (_initializationOrder)
+ {
+ _initializationOrder.Clear();
+ }
+ }
+}
+
+///
+/// Simulates a WebApplicationFactory that depends on a database container.
+/// The container MUST be initialized before this class's InitializeAsync is called,
+/// because InitializeAsync accesses the container's connection string.
+///
+public class MockWebApplicationFactory : IAsyncInitializer
+{
+ private static int _instanceCount;
+ private static int _initializeCount;
+
+ public static int InstanceCount => _instanceCount;
+ public static int InitializeCount => _initializeCount;
+
+ public int InstanceId { get; }
+ public bool IsInitialized { get; private set; }
+ public string? ConfiguredConnectionString { get; private set; }
+
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required MockDatabaseContainer Database { get; init; }
+
+ public MockWebApplicationFactory()
+ {
+ InstanceId = Interlocked.Increment(ref _instanceCount);
+ }
+
+ public Task InitializeAsync()
+ {
+ Interlocked.Increment(ref _initializeCount);
+
+ // This is the critical part: we need to access the container's connection string
+ // during initialization. If the container isn't initialized yet, this will throw.
+ ConfiguredConnectionString = Database.GetConnectionString();
+ IsInitialized = true;
+
+ Console.WriteLine($"[MockWebApplicationFactory #{InstanceId}] InitializeAsync called - Using connection: {ConfiguredConnectionString}");
+ return Task.CompletedTask;
+ }
+
+ public static void Reset()
+ {
+ _instanceCount = 0;
+ _initializeCount = 0;
+ }
+}
+
+#endregion
+
+#region Tests
+
+///
+/// Tests that nested IAsyncInitializer dependencies are initialized before the parent's InitializeAsync.
+/// This is the exact pattern from issue #4032.
+///
+[NotInParallel]
+[EngineTest(ExpectedResult.Pass)]
+public class NestedAsyncInitializerOrderTests
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required MockWebApplicationFactory Factory { get; init; }
+
+ [Before(Class)]
+ public static void ResetCounters()
+ {
+ MockDatabaseContainer.Reset();
+ MockWebApplicationFactory.Reset();
+ }
+
+ [Test]
+ public async Task Factory_ShouldBeInitialized()
+ {
+ // The factory should be initialized (which means the database must have been initialized first)
+ await Assert.That(Factory.IsInitialized).IsTrue()
+ .Because("the factory's InitializeAsync should have been called");
+ }
+
+ [Test]
+ public async Task Factory_ShouldHaveConfiguredConnectionString()
+ {
+ // The factory should have captured the connection string during initialization
+ await Assert.That(Factory.ConfiguredConnectionString).IsNotNull()
+ .Because("the factory should have accessed the database connection string during InitializeAsync");
+ }
+
+ [Test]
+ public async Task Database_ShouldBeInitializedBeforeFactory()
+ {
+ // The database should be initialized
+ await Assert.That(Factory.Database.IsInitialized).IsTrue()
+ .Because("the database container should be initialized before the factory accesses it");
+ }
+
+ [Test]
+ public async Task Database_InitializeCount_ShouldBeOne()
+ {
+ // The database should only be initialized once (shared per test session)
+ await Assert.That(MockDatabaseContainer.InitializeCount).IsEqualTo(1)
+ .Because("the database container is shared PerTestSession and should only be initialized once");
+ }
+
+ [Test]
+ public async Task Factory_InitializeCount_ShouldBeOne()
+ {
+ // The factory should only be initialized once (shared per test session)
+ await Assert.That(MockWebApplicationFactory.InitializeCount).IsEqualTo(1)
+ .Because("the factory is shared PerTestSession and should only be initialized once");
+ }
+}
+
+///
+/// Tests deep nesting (3 levels) of IAsyncInitializer dependencies.
+/// Uses a shared static list to track initialization order reliably.
+///
+public static class DeepNestingInitializationTracker
+{
+ private static readonly List _initializationOrder = [];
+ private static readonly object _lock = new();
+
+ public static void RecordInitialization(string name)
+ {
+ lock (_lock)
+ {
+ _initializationOrder.Add(name);
+ }
+ }
+
+ public static IReadOnlyList GetOrder()
+ {
+ lock (_lock)
+ {
+ return _initializationOrder.ToList();
+ }
+ }
+
+ public static void Reset()
+ {
+ lock (_lock)
+ {
+ _initializationOrder.Clear();
+ }
+ }
+}
+
+public class DeepNestedDependency : IAsyncInitializer
+{
+ private static int _instanceCount;
+ private static int _initializeCount;
+
+ public static int InstanceCount => _instanceCount;
+ public static int InitializeCount => _initializeCount;
+
+ public int InstanceId { get; }
+ public bool IsInitialized { get; private set; }
+ public string? Value { get; private set; }
+
+ public DeepNestedDependency()
+ {
+ InstanceId = Interlocked.Increment(ref _instanceCount);
+ }
+
+ public Task InitializeAsync()
+ {
+ Interlocked.Increment(ref _initializeCount);
+ IsInitialized = true;
+ Value = $"DeepValue_{InstanceId}";
+ DeepNestingInitializationTracker.RecordInitialization("Deep");
+ Console.WriteLine($"[DeepNestedDependency #{InstanceId}] InitializeAsync called");
+ return Task.CompletedTask;
+ }
+
+ public string GetValue()
+ {
+ if (!IsInitialized || Value == null)
+ {
+ throw new InvalidOperationException($"DeepNestedDependency #{InstanceId} is not initialized!");
+ }
+
+ return Value;
+ }
+
+ public static void Reset()
+ {
+ _instanceCount = 0;
+ _initializeCount = 0;
+ }
+}
+
+public class MiddleDependency : IAsyncInitializer
+{
+ private static int _instanceCount;
+ private static int _initializeCount;
+
+ public static int InstanceCount => _instanceCount;
+ public static int InitializeCount => _initializeCount;
+
+ public int InstanceId { get; }
+ public bool IsInitialized { get; private set; }
+ public string? CombinedValue { get; private set; }
+
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required DeepNestedDependency DeepDependency { get; init; }
+
+ public MiddleDependency()
+ {
+ InstanceId = Interlocked.Increment(ref _instanceCount);
+ }
+
+ public Task InitializeAsync()
+ {
+ Interlocked.Increment(ref _initializeCount);
+
+ // Access the deep dependency - must be initialized first
+ CombinedValue = $"Middle_{InstanceId}_{DeepDependency.GetValue()}";
+ IsInitialized = true;
+ DeepNestingInitializationTracker.RecordInitialization("Middle");
+ Console.WriteLine($"[MiddleDependency #{InstanceId}] InitializeAsync called - CombinedValue: {CombinedValue}");
+ return Task.CompletedTask;
+ }
+
+ public string GetCombinedValue()
+ {
+ if (!IsInitialized || CombinedValue == null)
+ {
+ throw new InvalidOperationException($"MiddleDependency #{InstanceId} is not initialized!");
+ }
+
+ return CombinedValue;
+ }
+
+ public static void Reset()
+ {
+ _instanceCount = 0;
+ _initializeCount = 0;
+ }
+}
+
+public class TopLevelDependency : IAsyncInitializer
+{
+ private static int _instanceCount;
+ private static int _initializeCount;
+
+ public static int InstanceCount => _instanceCount;
+ public static int InitializeCount => _initializeCount;
+
+ public int InstanceId { get; }
+ public bool IsInitialized { get; private set; }
+ public string? FinalValue { get; private set; }
+
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required MiddleDependency MiddleDependency { get; init; }
+
+ public TopLevelDependency()
+ {
+ InstanceId = Interlocked.Increment(ref _instanceCount);
+ }
+
+ public Task InitializeAsync()
+ {
+ Interlocked.Increment(ref _initializeCount);
+
+ // Access the middle dependency - must be initialized first
+ FinalValue = $"Top_{InstanceId}_{MiddleDependency.GetCombinedValue()}";
+ IsInitialized = true;
+ DeepNestingInitializationTracker.RecordInitialization("Top");
+ Console.WriteLine($"[TopLevelDependency #{InstanceId}] InitializeAsync called - FinalValue: {FinalValue}");
+ return Task.CompletedTask;
+ }
+
+ public static void Reset()
+ {
+ _instanceCount = 0;
+ _initializeCount = 0;
+ }
+}
+
+///
+/// Tests 3-level deep nesting of IAsyncInitializer dependencies.
+/// Order must be: DeepNestedDependency -> MiddleDependency -> TopLevelDependency
+///
+[NotInParallel]
+[EngineTest(ExpectedResult.Pass)]
+public class DeepNestedAsyncInitializerOrderTests
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required TopLevelDependency TopLevel { get; init; }
+
+ [Before(Class)]
+ public static void ResetCounters()
+ {
+ DeepNestedDependency.Reset();
+ MiddleDependency.Reset();
+ TopLevelDependency.Reset();
+ DeepNestingInitializationTracker.Reset();
+ }
+
+ [Test]
+ public async Task AllLevels_ShouldBeInitialized()
+ {
+ await Assert.That(TopLevel.IsInitialized).IsTrue();
+ await Assert.That(TopLevel.MiddleDependency.IsInitialized).IsTrue();
+ await Assert.That(TopLevel.MiddleDependency.DeepDependency.IsInitialized).IsTrue();
+ }
+
+ [Test]
+ public async Task InitializationOrder_ShouldBeDeepestFirst()
+ {
+ // Get the recorded initialization order
+ var order = DeepNestingInitializationTracker.GetOrder();
+
+ // Deep should be initialized first, then Middle, then Top
+ await Assert.That(order).HasCount().EqualTo(3)
+ .Because("there should be exactly 3 initializations");
+
+ await Assert.That(order[0]).IsEqualTo("Deep")
+ .Because("the deepest dependency should be initialized first");
+
+ await Assert.That(order[1]).IsEqualTo("Middle")
+ .Because("the middle dependency should be initialized second");
+
+ await Assert.That(order[2]).IsEqualTo("Top")
+ .Because("the top-level dependency should be initialized last");
+ }
+
+ [Test]
+ public async Task FinalValue_ShouldContainAllLevels()
+ {
+ // The final value should contain data from all three levels
+ await Assert.That(TopLevel.FinalValue).Contains("Top_");
+ await Assert.That(TopLevel.FinalValue).Contains("Middle_");
+ await Assert.That(TopLevel.FinalValue).Contains("DeepValue_");
+ }
+
+ [Test]
+ public async Task EachLevel_ShouldBeInitializedOnce()
+ {
+ await Assert.That(DeepNestedDependency.InitializeCount).IsEqualTo(1);
+ await Assert.That(MiddleDependency.InitializeCount).IsEqualTo(1);
+ await Assert.That(TopLevelDependency.InitializeCount).IsEqualTo(1);
+ }
+}
+
+#endregion