diff --git a/TUnit.Core/Helpers/ClassConstructorHelper.cs b/TUnit.Core/Helpers/ClassConstructorHelper.cs index 136a6132b1..8430cfb63b 100644 --- a/TUnit.Core/Helpers/ClassConstructorHelper.cs +++ b/TUnit.Core/Helpers/ClassConstructorHelper.cs @@ -50,9 +50,9 @@ public static class ClassConstructorHelper return null; } - // Use the ClassConstructor to create the instance - var classConstructorType = classConstructorAttribute.ClassConstructorType; - var classConstructor = (IClassConstructor)Activator.CreateInstance(classConstructorType)!; + // Reuse existing ClassConstructor if already set, otherwise create new instance + var classConstructor = testBuilderContext.ClassConstructor + ?? (IClassConstructor)Activator.CreateInstance(classConstructorAttribute.ClassConstructorType)!; testBuilderContext.ClassConstructor = classConstructor; diff --git a/TUnit.Core/TestBuilderContext.cs b/TUnit.Core/TestBuilderContext.cs index cac0481205..25b20c65d4 100644 --- a/TUnit.Core/TestBuilderContext.cs +++ b/TUnit.Core/TestBuilderContext.cs @@ -57,7 +57,11 @@ internal static TestBuilderContext FromTestContext(TestContext testContext, IDat { return new TestBuilderContext { - Events = testContext.InternalEvents, TestMetadata = testContext.Metadata.TestDetails.MethodMetadata, DataSourceAttribute = dataSourceAttribute, StateBag = testContext.StateBag.Items, + Events = testContext.InternalEvents, + TestMetadata = testContext.Metadata.TestDetails.MethodMetadata, + DataSourceAttribute = dataSourceAttribute, + StateBag = testContext.StateBag.Items, + ClassConstructor = testContext.ClassConstructor, }; } } diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs index a854f4e2df..cc6da989b9 100644 --- a/TUnit.Engine/Building/TestBuilder.cs +++ b/TUnit.Engine/Building/TestBuilder.cs @@ -290,7 +290,8 @@ public async Task> BuildTestsFromMetadataAsy Events = new TestContextEvents(), StateBag = new ConcurrentDictionary(), DataSourceAttribute = methodDataSource, - InitializedAttributes = testBuilderContext.InitializedAttributes // Preserve attributes from parent context + InitializedAttributes = testBuilderContext.InitializedAttributes, // Preserve attributes from parent context + ClassConstructor = testBuilderContext.ClassConstructor // Preserve ClassConstructor for instance creation }; classData = DataUnwrapper.Unwrap(await classDataFactory() ?? []); diff --git a/TUnit.TestProject/Bugs/3939/Tests.cs b/TUnit.TestProject/Bugs/3939/Tests.cs new file mode 100644 index 0000000000..215a5be18c --- /dev/null +++ b/TUnit.TestProject/Bugs/3939/Tests.cs @@ -0,0 +1,53 @@ +using System.Diagnostics.CodeAnalysis; +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3939; + +/// +/// Regression test for issue #3939: IClassConstructor and ITestEndEventReceiver +/// should use the same instance so that state can be shared (e.g., DI scope disposal). +/// +public sealed class ScopedClassConstructor : IClassConstructor, ITestEndEventReceiver +{ + private object? _scope; + + public Task Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, ClassConstructorMetadata classConstructorMetadata) + { + // Simulate creating a DI scope - this should be available in OnTestEnd + _scope = new object(); + return Task.FromResult(new Tests()); + } + + public ValueTask OnTestEnd(TestContext context) + { + // This should be called on the SAME instance that Create() was called on + // If _scope is null, it means a different instance was used - this is the bug! + if (_scope == null) + { + throw new InvalidOperationException( + "Issue #3939: _scope was null in OnTestEnd(), indicating a different IClassConstructor " + + "instance was used than the one where Create() was called. " + + "IClassConstructor and ITestEndEventReceiver must use the same instance."); + } + + // Simulate disposing the scope + _scope = null; + return default; + } + + public int Order => 0; +} + +[EngineTest(ExpectedResult.Pass)] +[ClassConstructor] +public sealed class Tests +{ + [Test] + public Task TestMethod() + { + // The actual test - just verifies the test runs + // The real assertion is in OnTestEnd - it will throw if different instances are used + return Task.CompletedTask; + } +}