diff --git a/TUnit.Core/Interfaces/IAsyncDiscoveryInitializer.cs b/TUnit.Core/Interfaces/IAsyncDiscoveryInitializer.cs
new file mode 100644
index 0000000000..8c9f4f1dfb
--- /dev/null
+++ b/TUnit.Core/Interfaces/IAsyncDiscoveryInitializer.cs
@@ -0,0 +1,28 @@
+namespace TUnit.Core.Interfaces;
+
+///
+/// Defines a contract for types that require asynchronous initialization during test discovery.
+///
+///
+///
+/// Unlike which runs during test execution,
+/// implementations of this interface are initialized during the test discovery phase.
+/// This enables data sources (such as InstanceMethodDataSource ) to access
+/// fully-initialized objects when generating test cases.
+///
+///
+/// Common use cases include:
+///
+/// Starting Docker containers before test case enumeration
+/// Connecting to databases to discover parameterized test data
+/// Initializing fixtures that provide data for test case generation
+///
+///
+///
+/// This interface extends , meaning the same
+/// method is used. The framework
+/// guarantees exactly-once initialization semantics - objects will not be
+/// re-initialized during test execution.
+///
+///
+public interface IAsyncDiscoveryInitializer : IAsyncInitializer;
diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs
index b3ef936b88..30dc1bcb7e 100644
--- a/TUnit.Engine/Building/TestBuilder.cs
+++ b/TUnit.Engine/Building/TestBuilder.cs
@@ -66,6 +66,30 @@ private async Task InitializeDeferredClassDataAsync(object?[] classData)
}
}
+ ///
+ /// Initializes any IAsyncDiscoveryInitializer objects in class data during test discovery.
+ /// This is called BEFORE method data sources are evaluated, enabling data sources
+ /// to access initialized shared objects (like Docker containers).
+ ///
+ private static async Task InitializeDiscoveryObjectsAsync(object?[] classData)
+ {
+ if (classData == null || classData.Length == 0)
+ {
+ return;
+ }
+
+ foreach (var data in classData)
+ {
+ if (data is IAsyncDiscoveryInitializer)
+ {
+ // Uses ObjectInitializer which handles deduplication.
+ // This also prevents double-init during execution since ObjectInitializer
+ // tracks initialized objects.
+ await ObjectInitializer.InitializeAsync(data);
+ }
+ }
+ }
+
private async Task CreateInstance(TestMetadata metadata, Type[] resolvedClassGenericArgs, object?[] classData, TestBuilderContext builderContext)
{
// Initialize any deferred IAsyncInitializer objects in class data
@@ -206,6 +230,10 @@ public async Task> BuildTestsFromMetadataAsy
var classDataResult = await classDataFactory() ?? [];
var classData = DataUnwrapper.Unwrap(classDataResult);
+ // Initialize IAsyncDiscoveryInitializer objects before method data sources are evaluated.
+ // This enables InstanceMethodDataSource to access initialized shared objects.
+ await InitializeDiscoveryObjectsAsync(classData);
+
var needsInstanceForMethodDataSources = metadata.DataSources.Any(ds => ds is IAccessesInstanceData);
object? instanceForMethodDataSources = null;
@@ -265,6 +293,12 @@ await _objectLifecycleService.RegisterObjectAsync(
tempObjectBag,
metadata.MethodMetadata,
tempEvents);
+
+ // Initialize the test class instance if it implements IAsyncDiscoveryInitializer
+ if (instanceForMethodDataSources is IAsyncDiscoveryInitializer)
+ {
+ await ObjectInitializer.InitializeAsync(instanceForMethodDataSources);
+ }
}
}
catch (Exception ex)
@@ -313,6 +347,9 @@ await _objectLifecycleService.RegisterObjectAsync(
classData = DataUnwrapper.Unwrap(await classDataFactory() ?? []);
var methodData = DataUnwrapper.UnwrapWithTypes(await methodDataFactory() ?? [], metadata.MethodMetadata.Parameters);
+ // Initialize any IAsyncDiscoveryInitializer objects in method data
+ await InitializeDiscoveryObjectsAsync(methodData);
+
// For concrete generic instantiations, check if the data is compatible with the expected types
if (metadata.GenericMethodTypeArguments is { Length: > 0 })
{
@@ -1386,6 +1423,10 @@ public async IAsyncEnumerable BuildTestsStreamingAsync(
var classData = DataUnwrapper.Unwrap(await classDataFactory() ?? []);
+ // Initialize IAsyncDiscoveryInitializer objects before method data sources are evaluated.
+ // This enables InstanceMethodDataSource to access initialized shared objects.
+ await InitializeDiscoveryObjectsAsync(classData);
+
// Handle instance creation for method data sources
var needsInstanceForMethodDataSources = metadata.DataSources.Any(ds => ds is IAccessesInstanceData);
object? instanceForMethodDataSources = null;
@@ -1410,6 +1451,12 @@ await _objectLifecycleService.RegisterObjectAsync(
tempObjectBag,
metadata.MethodMetadata,
tempEvents);
+
+ // Initialize the test class instance if it implements IAsyncDiscoveryInitializer
+ if (instanceForMethodDataSources is IAsyncDiscoveryInitializer)
+ {
+ await ObjectInitializer.InitializeAsync(instanceForMethodDataSources);
+ }
}
// Stream through method data sources
@@ -1520,6 +1567,9 @@ await _objectLifecycleService.RegisterObjectAsync(
var methodData = DataUnwrapper.UnwrapWithTypes(await methodDataFactory() ?? [], metadata.MethodMetadata.Parameters);
+ // Initialize any IAsyncDiscoveryInitializer objects in method data
+ await InitializeDiscoveryObjectsAsync(methodData);
+
// Check data compatibility for generic methods
if (metadata.GenericMethodTypeArguments is { Length: > 0 })
{
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index a445e9e1f2..33684f6146 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -2199,6 +2199,7 @@ namespace .Interfaces
{
.<> GenerateDataFactoriesAsync(.DataSourceContext context, .CancellationToken cancellationToken = default);
}
+ public interface IAsyncDiscoveryInitializer : . { }
public interface IAsyncInitializer
{
. InitializeAsync();
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index a1f6b35877..d97c4f38b5 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -2199,6 +2199,7 @@ namespace .Interfaces
{
.<> GenerateDataFactoriesAsync(.DataSourceContext context, .CancellationToken cancellationToken = default);
}
+ public interface IAsyncDiscoveryInitializer : . { }
public interface IAsyncInitializer
{
. InitializeAsync();
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index e295333e1a..5280c9595c 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -2199,6 +2199,7 @@ namespace .Interfaces
{
.<> GenerateDataFactoriesAsync(.DataSourceContext context, .CancellationToken cancellationToken = default);
}
+ public interface IAsyncDiscoveryInitializer : . { }
public interface IAsyncInitializer
{
. InitializeAsync();
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
index ee302f89a4..538bd3ca00 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
@@ -2131,6 +2131,7 @@ namespace .Interfaces
{
.<> GenerateDataFactoriesAsync(.DataSourceContext context, .CancellationToken cancellationToken = default);
}
+ public interface IAsyncDiscoveryInitializer : . { }
public interface IAsyncInitializer
{
. InitializeAsync();
diff --git a/TUnit.TestProject/Bugs/3997/Tests.cs b/TUnit.TestProject/Bugs/3997/Tests.cs
new file mode 100644
index 0000000000..9fe62ef189
--- /dev/null
+++ b/TUnit.TestProject/Bugs/3997/Tests.cs
@@ -0,0 +1,58 @@
+using TUnit.Core;
+using TUnit.Core.Interfaces;
+using TUnit.TestProject.Attributes;
+
+namespace TUnit.TestProject.Bugs._3997;
+
+///
+/// Simulates a data source (like a Docker container) that needs initialization during test discovery
+/// so that InstanceMethodDataSource can access its data when generating test cases.
+///
+public class SimulatedContainer : IAsyncDiscoveryInitializer
+{
+ private readonly List _data = [];
+ public bool IsInitialized { get; private set; }
+
+ public IReadOnlyList Data => _data;
+
+ public Task InitializeAsync()
+ {
+ if (IsInitialized)
+ {
+ throw new InvalidOperationException("Container already initialized! InitializeAsync should only be called once.");
+ }
+
+ // Simulate container startup and data population
+ _data.AddRange(["TestCase1", "TestCase2", "TestCase3"]);
+ IsInitialized = true;
+ return Task.CompletedTask;
+ }
+}
+
+///
+/// Tests that IAsyncDiscoveryInitializer is called during test discovery,
+/// allowing InstanceMethodDataSource to access initialized data.
+///
+[EngineTest(ExpectedResult.Pass)]
+public class DiscoveryInitializerTests
+{
+ [ClassDataSource(Shared = SharedType.PerClass)]
+ public required SimulatedContainer Container { get; init; }
+
+ ///
+ /// This property provides test data from the initialized container.
+ /// The container MUST be initialized during discovery before this is evaluated.
+ ///
+ public IEnumerable TestCases => Container.Data;
+
+ [Test]
+ [InstanceMethodDataSource(nameof(TestCases))]
+ public async Task TestWithContainerData(string testCase)
+ {
+ // Container should be initialized
+ await Assert.That(Container.IsInitialized).IsTrue();
+
+ // testCase should be one of the container's data items
+ await Assert.That(Container.Data).Contains(testCase);
+ }
+}