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
17 changes: 15 additions & 2 deletions TUnit.Core/Attributes/TestData/ClassDataSources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Reflection;
using System.Runtime.ExceptionServices;
using TUnit.Core.Data;
using TUnit.Core.Interfaces;

namespace TUnit.Core;

Expand Down Expand Up @@ -46,7 +47,7 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys)
SharedType.None => Create<T>(),
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()
};
Expand All @@ -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>()
{
Expand Down
27 changes: 27 additions & 0 deletions TUnit.Core/Interfaces/IKeyedDataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace TUnit.Core.Interfaces;

/// <summary>
/// Defines a contract for data source types that need to know the key they were created with
/// when using <see cref="SharedType.Keyed"/> sharing.
/// </summary>
/// <remarks>
/// <para>
/// When a class is used as a <c>[ClassDataSource]</c> with <see cref="SharedType.Keyed"/>,
/// implementing this interface allows the instance to receive its sharing key before
/// <see cref="IAsyncInitializer.InitializeAsync"/> is called.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public interface IKeyedDataSource
{
/// <summary>
/// Gets or sets the sharing key that this instance was created with.
/// </summary>
/// <remarks>
/// Set by the TUnit framework after construction but before <see cref="IAsyncInitializer.InitializeAsync"/> is called.
/// </remarks>
string Key { get; set; }
}
25 changes: 23 additions & 2 deletions TUnit.Core/SharedDataSources.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using TUnit.Core.Helpers;
using TUnit.Core.Interfaces;

namespace TUnit.Core;

Expand Down Expand Up @@ -181,7 +182,17 @@ private static T GetForKey<T>(string? key, Func<T> 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<object?> factory)
Expand All @@ -191,7 +202,17 @@ private static T GetForKey<T>(string? key, Func<T> 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<T>(Type? testClassType, Func<T> factory)
Expand Down
21 changes: 21 additions & 0 deletions TUnit.Engine.Tests/KeyedDataSourceTests.cs
Original file line number Diff line number Diff line change
@@ -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)
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2362,6 +2362,10 @@ namespace .Interfaces
. OnHookRegistered(.HookRegisteredContext context);
}
public interface IInfersType<T> { }
public interface IKeyedDataSource
{
string Key { get; set; }
}
public interface ILastTestInAssemblyEventReceiver : .
{
. OnLastTestInAssembly(.AssemblyHookContext context, .TestContext testContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2362,6 +2362,10 @@ namespace .Interfaces
. OnHookRegistered(.HookRegisteredContext context);
}
public interface IInfersType<T> { }
public interface IKeyedDataSource
{
string Key { get; set; }
}
public interface ILastTestInAssemblyEventReceiver : .
{
. OnLastTestInAssembly(.AssemblyHookContext context, .TestContext testContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2362,6 +2362,10 @@ namespace .Interfaces
. OnHookRegistered(.HookRegisteredContext context);
}
public interface IInfersType<T> { }
public interface IKeyedDataSource
{
string Key { get; set; }
}
public interface ILastTestInAssemblyEventReceiver : .
{
. OnLastTestInAssembly(.AssemblyHookContext context, .TestContext testContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2314,6 +2314,10 @@ namespace .Interfaces
. OnHookRegistered(.HookRegisteredContext context);
}
public interface IInfersType<T> { }
public interface IKeyedDataSource
{
string Key { get; set; }
}
public interface ILastTestInAssemblyEventReceiver : .
{
. OnLastTestInAssembly(.AssemblyHookContext context, .TestContext testContext);
Expand Down
57 changes: 57 additions & 0 deletions TUnit.TestProject/KeyedDataSourceTests.cs
Original file line number Diff line number Diff line change
@@ -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<KeyAwareFixture> AlphaInstances = [];

[Test]
[ClassDataSource<KeyAwareFixture>(Shared = SharedType.Keyed, Key = "alpha")]
public async Task Key_IsSetToAlpha(KeyAwareFixture fixture)
{
AlphaInstances.Add(fixture);
await Assert.That(fixture.Key).IsEqualTo("alpha");
}

[Test]
[ClassDataSource<KeyAwareFixture>(Shared = SharedType.Keyed, Key = "beta")]
public async Task Key_IsSetToBeta(KeyAwareFixture fixture)
{
await Assert.That(fixture.Key).IsEqualTo("beta");
}

[Test]
[ClassDataSource<KeyAwareFixture>(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]);
}
}
Loading