Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions TUnit.Core/Initialization/TestObjectInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public async Task InitializeArgumentsAsync(
object?[] arguments,
Dictionary<string, object?> objectBag,
MethodMetadata methodMetadata,
TestContextEvents events)
TestContextEvents events,
bool isRegistrationPhase = false)
{
if (arguments == null || arguments.Length == 0)
{
Expand All @@ -82,7 +83,8 @@ public async Task InitializeArgumentsAsync(
ObjectBag = objectBag,
MethodMetadata = methodMetadata,
Events = events,
TestContext = TestContext.Current
TestContext = TestContext.Current,
IsRegistrationPhase = isRegistrationPhase
};

// Process arguments in parallel for performance
Expand Down Expand Up @@ -139,7 +141,12 @@ await _propertyInjectionService.InjectPropertiesIntoObjectAsync(
// Step 2: Object Initialization (IAsyncInitializer)
if (instance is IAsyncInitializer asyncInitializer)
{
await ObjectInitializer.InitializeAsync(instance);
// During registration phase, only initialize data source attributes.
// Other IAsyncInitializer objects are deferred until test execution.
if (!context.IsRegistrationPhase || instance is IDataSourceAttribute)
{
await ObjectInitializer.InitializeAsync(instance);
}
}

// Step 3: Tracking (if not already tracked)
Expand Down Expand Up @@ -208,6 +215,7 @@ private class InitializationContext
public MethodMetadata? MethodMetadata { get; set; }
public TestContextEvents Events { get; set; } = null!;
public TestContext? TestContext { get; set; }
public bool IsRegistrationPhase { get; set; }
}
}

Expand Down
25 changes: 25 additions & 0 deletions TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,33 @@ public TestBuilder(
_dataSourceInitializer = dataSourceInitializer;
}

/// <summary>
/// Initializes any IAsyncInitializer objects in class data that were deferred during registration.
/// </summary>
private async Task InitializeDeferredClassDataAsync(object?[] classData)
{
if (classData == null || classData.Length == 0)
{
return;
}

foreach (var data in classData)
{
if (data is IAsyncInitializer asyncInitializer && data is not IDataSourceAttribute)
{
if (!ObjectInitializer.IsInitialized(data))
{
await ObjectInitializer.InitializeAsync(data);
}
}
}
}

private async Task<object> CreateInstance(TestMetadata metadata, Type[] resolvedClassGenericArgs, object?[] classData, TestBuilderContext builderContext)
{
// Initialize any deferred IAsyncInitializer objects in class data
await InitializeDeferredClassDataAsync(classData);

// First try to create instance with ClassConstructor attribute
// Use attributes from context if available
var attributes = builderContext.InitializedAttributes ?? metadata.AttributeFactory();
Expand Down
13 changes: 7 additions & 6 deletions TUnit.Engine/Services/TestArgumentTrackingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,21 @@ public async ValueTask OnTestRegistered(TestRegisteredContext context)
var classArguments = testContext.TestDetails.TestClassArguments;
var methodArguments = testContext.TestDetails.TestMethodArguments;

// Use centralized TestObjectInitializer for all initialization
// Initialize class arguments
// Initialize class arguments (registration phase)
await _testObjectInitializer.InitializeArgumentsAsync(
classArguments,
testContext.ObjectBag,
testContext.TestDetails.MethodMetadata,
testContext.Events);

// Initialize method arguments
testContext.Events,
isRegistrationPhase: true);

// Initialize method arguments (registration phase)
await _testObjectInitializer.InitializeArgumentsAsync(
methodArguments,
testContext.ObjectBag,
testContext.TestDetails.MethodMetadata,
testContext.Events);
testContext.Events,
isRegistrationPhase: true);

// Track all constructor and method arguments
// Note: TestObjectInitializer already handles tracking, but we ensure it here for clarity
Expand Down
3 changes: 3 additions & 0 deletions TUnit.Engine/Services/TestExecution/TestCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ private async Task ExecuteTestInternalAsync(AbstractExecutableTest test, Cancell
test.Context.Dependencies.Add(dependency);
}

// Ensure TestSession hooks run before creating test instances
await _testExecutor.EnsureTestSessionHooksExecutedAsync();

test.Context.TestDetails.ClassInstance = await test.CreateInstanceAsync();

// Check if this test should be skipped (after creating instance)
Expand Down
17 changes: 13 additions & 4 deletions TUnit.Engine/TestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ public TestExecutor(
}


/// <summary>
/// Ensures that Before(TestSession) hooks have been executed.
/// This is called before creating test instances to ensure resources are available.
/// </summary>
public async Task EnsureTestSessionHooksExecutedAsync()
{
// Get or create and cache Before hooks - these run only once
await _beforeHookTaskCache.GetOrCreateBeforeTestSessionTask(() =>
_hookExecutor.ExecuteBeforeTestSessionHooksAsync(CancellationToken.None)).ConfigureAwait(false);
}

/// <summary>
/// Creates a test executor delegate that wraps the provided executor with hook orchestration.
/// Uses focused services that follow SRP to manage lifecycle and execution.
Expand All @@ -48,10 +59,8 @@ public async Task ExecuteAsync(AbstractExecutableTest executableTest, Cancellati

try
{
// Get or create and cache Before hooks - these run only once
// We use cached delegates to prevent lambda capture issues
// Event receivers will be handled separately with their own internal coordination
await _beforeHookTaskCache.GetOrCreateBeforeTestSessionTask(() => _hookExecutor.ExecuteBeforeTestSessionHooksAsync(CancellationToken.None)).ConfigureAwait(false);
// Ensure TestSession hooks have been executed
await EnsureTestSessionHooksExecutedAsync().ConfigureAwait(false);

// Event receivers have their own internal coordination to run once
await _eventReceiverOrchestrator.InvokeFirstTestInSessionEventReceiversAsync(
Expand Down
Loading