diff --git a/TUnit.Core/Attributes/TestData/ClassDataSources.cs b/TUnit.Core/Attributes/TestData/ClassDataSources.cs index d6fc652455..b90b8a3cd4 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSources.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSources.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Runtime.ExceptionServices; using TUnit.Core.Data; +using TUnit.Core.Interfaces; namespace TUnit.Core; @@ -46,7 +47,7 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys) SharedType.None => Create(), SharedType.PerTestSession => (T) TestDataContainer.GetGlobalInstance(typeof(T), _ => Create(typeof(T)))!, SharedType.PerClass => (T) TestDataContainer.GetInstanceForClass(testClassType, typeof(T), _ => Create(typeof(T)))!, - SharedType.Keyed => (T) TestDataContainer.GetInstanceForKey(key, typeof(T), _ => Create(typeof(T)))!, + SharedType.Keyed => (T) TestDataContainer.GetInstanceForKey(key, typeof(T), _ => CreateWithKey(typeof(T), key))!, SharedType.PerAssembly => (T) TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, typeof(T), _ => Create(typeof(T)))!, _ => throw new ArgumentOutOfRangeException() }; @@ -59,12 +60,24 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys) SharedType.None => Create(type), SharedType.PerTestSession => TestDataContainer.GetGlobalInstance(type, _ => Create(type)), SharedType.PerClass => TestDataContainer.GetInstanceForClass(testClassType, type, _ => Create(type)), - SharedType.Keyed => TestDataContainer.GetInstanceForKey(key!, type, _ => Create(type)), + SharedType.Keyed => TestDataContainer.GetInstanceForKey(key!, type, _ => CreateWithKey(type, key!)), SharedType.PerAssembly => TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, type, _ => Create(type)), _ => throw new ArgumentOutOfRangeException() }; } + private static object CreateWithKey([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type, string key) + { + var instance = Create(type); + + if (instance is IKeyedDataSource keyed) + { + keyed.Key = key; + } + + return instance; + } + [return: NotNull] private static T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>() { diff --git a/TUnit.Core/Interfaces/IKeyedDataSource.cs b/TUnit.Core/Interfaces/IKeyedDataSource.cs new file mode 100644 index 0000000000..ce0c29d3c1 --- /dev/null +++ b/TUnit.Core/Interfaces/IKeyedDataSource.cs @@ -0,0 +1,27 @@ +namespace TUnit.Core.Interfaces; + +/// +/// Defines a contract for data source types that need to know the key they were created with +/// when using sharing. +/// +/// +/// +/// When a class is used as a [ClassDataSource] with , +/// implementing this interface allows the instance to receive its sharing key before +/// is called. +/// +/// +/// This is useful when a single fixture type needs to behave differently depending on which +/// key it was created for, without requiring separate subclasses per key variant. +/// +/// +public interface IKeyedDataSource +{ + /// + /// Gets or sets the sharing key that this instance was created with. + /// + /// + /// Set by the TUnit framework after construction but before is called. + /// + string Key { get; set; } +} diff --git a/TUnit.Core/SharedDataSources.cs b/TUnit.Core/SharedDataSources.cs index 27ec9236da..a6e6fa4aae 100644 --- a/TUnit.Core/SharedDataSources.cs +++ b/TUnit.Core/SharedDataSources.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using TUnit.Core.Helpers; +using TUnit.Core.Interfaces; namespace TUnit.Core; @@ -181,7 +182,17 @@ private static T GetForKey(string? key, Func factory) throw new ArgumentNullException(nameof(key), "key is required when SharedType is Keyed."); } - return (T)TestDataContainer.GetInstanceForKey(key!, typeof(T), _ => factory()!)!; + return (T)TestDataContainer.GetInstanceForKey(key!, typeof(T), _ => + { + var instance = factory()!; + + if (instance is IKeyedDataSource keyed) + { + keyed.Key = key!; + } + + return instance; + })!; } private static object? GetForKey(Type type, string? key, Func factory) @@ -191,7 +202,17 @@ private static T GetForKey(string? key, Func factory) throw new ArgumentNullException(nameof(key), "key is required when SharedType is Keyed."); } - return TestDataContainer.GetInstanceForKey(key!, type, _ => factory()!); + return TestDataContainer.GetInstanceForKey(key!, type, _ => + { + var instance = factory()!; + + if (instance is IKeyedDataSource keyed) + { + keyed.Key = key!; + } + + return instance; + }); } private static T GetForAssembly(Type? testClassType, Func factory) diff --git a/TUnit.Engine.Tests/KeyedDataSourceTests.cs b/TUnit.Engine.Tests/KeyedDataSourceTests.cs new file mode 100644 index 0000000000..6ade30a536 --- /dev/null +++ b/TUnit.Engine.Tests/KeyedDataSourceTests.cs @@ -0,0 +1,21 @@ +using Shouldly; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +public class KeyedDataSourceTests(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task KeyedDataSourceTests_ShouldPass() + { + await RunTestsWithFilter( + "/*/*/KeyedDataSourceTests/*", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(4), + result => result.ResultSummary.Counters.Passed.ShouldBe(4), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + result => result.ResultSummary.Counters.NotExecuted.ShouldBe(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 76bad876d9..4e2e58a552 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 @@ -2362,6 +2362,10 @@ namespace .Interfaces . OnHookRegistered(.HookRegisteredContext context); } public interface IInfersType { } + public interface IKeyedDataSource + { + string Key { get; set; } + } public interface ILastTestInAssemblyEventReceiver : . { . OnLastTestInAssembly(.AssemblyHookContext context, .TestContext testContext); 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 72c3b943a5..f6c2d6dab9 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 @@ -2362,6 +2362,10 @@ namespace .Interfaces . OnHookRegistered(.HookRegisteredContext context); } public interface IInfersType { } + public interface IKeyedDataSource + { + string Key { get; set; } + } public interface ILastTestInAssemblyEventReceiver : . { . OnLastTestInAssembly(.AssemblyHookContext context, .TestContext testContext); 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 1caf4068ab..c4e4afa88c 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 @@ -2362,6 +2362,10 @@ namespace .Interfaces . OnHookRegistered(.HookRegisteredContext context); } public interface IInfersType { } + public interface IKeyedDataSource + { + string Key { get; set; } + } public interface ILastTestInAssemblyEventReceiver : . { . OnLastTestInAssembly(.AssemblyHookContext context, .TestContext testContext); 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 319cb11fed..a4644ef903 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 @@ -2314,6 +2314,10 @@ namespace .Interfaces . OnHookRegistered(.HookRegisteredContext context); } public interface IInfersType { } + public interface IKeyedDataSource + { + string Key { get; set; } + } public interface ILastTestInAssemblyEventReceiver : . { . OnLastTestInAssembly(.AssemblyHookContext context, .TestContext testContext); diff --git a/TUnit.TestProject/KeyedDataSourceTests.cs b/TUnit.TestProject/KeyedDataSourceTests.cs new file mode 100644 index 0000000000..d1528fa433 --- /dev/null +++ b/TUnit.TestProject/KeyedDataSourceTests.cs @@ -0,0 +1,57 @@ +using System.Diagnostics.CodeAnalysis; +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject; + +public class KeyAwareFixture : IKeyedDataSource, IAsyncInitializer +{ + public string Key { get; set; } = string.Empty; + + public string KeyDuringInit { get; private set; } = string.Empty; + + public Task InitializeAsync() + { + KeyDuringInit = Key; + return Task.CompletedTask; + } +} + +[EngineTest(ExpectedResult.Pass)] +[UnconditionalSuppressMessage("Usage", "TUnit0018:Test methods should not assign instance data")] +public class KeyedDataSourceTests +{ + private static readonly List AlphaInstances = []; + + [Test] + [ClassDataSource(Shared = SharedType.Keyed, Key = "alpha")] + public async Task Key_IsSetToAlpha(KeyAwareFixture fixture) + { + AlphaInstances.Add(fixture); + await Assert.That(fixture.Key).IsEqualTo("alpha"); + } + + [Test] + [ClassDataSource(Shared = SharedType.Keyed, Key = "beta")] + public async Task Key_IsSetToBeta(KeyAwareFixture fixture) + { + await Assert.That(fixture.Key).IsEqualTo("beta"); + } + + [Test] + [ClassDataSource(Shared = SharedType.Keyed, Key = "alpha")] + public async Task Key_IsAvailableDuringInitializeAsync(KeyAwareFixture fixture) + { + AlphaInstances.Add(fixture); + await Assert.That(fixture.KeyDuringInit).IsEqualTo("alpha"); + } + + [Test] + [DependsOn(nameof(Key_IsSetToAlpha))] + [DependsOn(nameof(Key_IsAvailableDuringInitializeAsync))] + public async Task SameKey_ReturnsSameInstance() + { + await Assert.That(AlphaInstances).HasCount().EqualTo(2); + await Assert.That(AlphaInstances[0]).IsSameReferenceAs(AlphaInstances[1]); + } +}