diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs index 38cf785996..628c8e6449 100644 --- a/TUnit.Engine/Building/TestBuilder.cs +++ b/TUnit.Engine/Building/TestBuilder.cs @@ -766,10 +766,10 @@ private async Task GetDataSourcesAsync(IDataSourceAttrib return [NoDataSource.Instance]; } - // Initialize all data sources to ensure properties are injected + // Inject properties into data sources during discovery (IAsyncInitializer deferred to execution) foreach (var dataSource in dataSources) { - await _objectLifecycleService.EnsureInitializedAsync(dataSource); + await _objectLifecycleService.InjectPropertiesAsync(dataSource); } return dataSources; @@ -783,16 +783,15 @@ private async Task GetDataSourcesAsync(IDataSourceAttrib IDataSourceAttribute dataSource, DataGeneratorMetadata dataGeneratorMetadata) { - // Ensure the data source is fully initialized before getting data rows - // This includes property injection and IAsyncInitializer.InitializeAsync - var initializedDataSource = await _objectLifecycleService.EnsureInitializedAsync( + // Inject properties into data source during discovery (IAsyncInitializer deferred to execution) + var propertyInjectedDataSource = await _objectLifecycleService.InjectPropertiesAsync( dataSource, dataGeneratorMetadata.TestBuilderContext.Current.StateBag, dataGeneratorMetadata.TestInformation, dataGeneratorMetadata.TestBuilderContext.Current.Events); - // Now get data rows from the initialized data source - await foreach (var dataRow in initializedDataSource.GetDataRowsAsync(dataGeneratorMetadata)) + // Now get data rows from the property-injected data source + await foreach (var dataRow in propertyInjectedDataSource.GetDataRowsAsync(dataGeneratorMetadata)) { yield return dataRow; } @@ -1017,14 +1016,13 @@ private async Task CreateFailedTestDetails(TestMetadata metadata, s private async Task InitializeAttributesAsync(Attribute[] attributes) { - // Initialize any attributes that need property injection or implement IAsyncInitializer - // This ensures they're fully initialized before being used + // Inject properties into data source attributes during discovery + // IAsyncInitializer.InitializeAsync is deferred to execution time foreach (var attribute in attributes) { if (attribute is IDataSourceAttribute dataSource) { - // Data source attributes need to be initialized with property injection - await _objectLifecycleService.EnsureInitializedAsync(dataSource); + await _objectLifecycleService.InjectPropertiesAsync(dataSource); } } diff --git a/TUnit.Engine/Services/ObjectLifecycleService.cs b/TUnit.Engine/Services/ObjectLifecycleService.cs index f554d07ea0..33d50d9b08 100644 --- a/TUnit.Engine/Services/ObjectLifecycleService.cs +++ b/TUnit.Engine/Services/ObjectLifecycleService.cs @@ -207,6 +207,30 @@ await EnsureInitializedAsync( #region Phase 3: Object Initialization + /// + /// Injects properties into an object without calling IAsyncInitializer. + /// Used during test discovery to prepare data sources without triggering async initialization. + /// + public async ValueTask InjectPropertiesAsync( + T obj, + ConcurrentDictionary? objectBag = null, + MethodMetadata? methodMetadata = null, + TestContextEvents? events = null) where T : notnull + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + objectBag ??= new ConcurrentDictionary(); + events ??= new TestContextEvents(); + + // Only inject properties, do not call IAsyncInitializer + await PropertyInjector.InjectPropertiesAsync(obj, objectBag, methodMetadata, events); + + return obj; + } + /// /// Ensures an object is fully initialized (property injection + IAsyncInitializer). /// Thread-safe with fast-path for already-initialized objects. diff --git a/TUnit.TestProject/Bugs/3992/IAsyncInitializerDiscoveryTests.cs b/TUnit.TestProject/Bugs/3992/IAsyncInitializerDiscoveryTests.cs new file mode 100644 index 0000000000..b0c579b0ac --- /dev/null +++ b/TUnit.TestProject/Bugs/3992/IAsyncInitializerDiscoveryTests.cs @@ -0,0 +1,41 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3992; + +/// +/// Regression test for issue #3992: IAsyncInitializer should not run during test discovery. +/// Verifies that IAsyncInitializer.InitializeAsync() is only called during test execution, +/// not during the discovery phase. +/// +[EngineTest(ExpectedResult.Pass)] +public class IAsyncInitializerDiscoveryTests +{ + private static int _initializationCount = 0; + + public class DataSourceWithAsyncInit : IAsyncInitializer + { + public bool IsInitialized { get; private set; } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializationCount); + IsInitialized = true; + Console.WriteLine($"InitializeAsync called (count: {_initializationCount})"); + return Task.CompletedTask; + } + } + + [Test] + [ClassDataSource] + public async Task DataSource_InitializeAsync_OnlyCalledDuringExecution(DataSourceWithAsyncInit dataSource) + { + // Verify that the data source was initialized + await Assert.That(dataSource.IsInitialized).IsTrue(); + + // Verify that InitializeAsync was called (at least once during execution) + await Assert.That(_initializationCount).IsGreaterThan(0); + + Console.WriteLine($"Test execution confirmed: InitializeAsync was called {_initializationCount} time(s)"); + } +}