diff --git a/TUnit.Core/Attributes/TestMetadata/DisplayNameAttribute.cs b/TUnit.Core/Attributes/TestMetadata/DisplayNameAttribute.cs index fb4b4cccff..f9cb349aee 100644 --- a/TUnit.Core/Attributes/TestMetadata/DisplayNameAttribute.cs +++ b/TUnit.Core/Attributes/TestMetadata/DisplayNameAttribute.cs @@ -5,30 +5,37 @@ namespace TUnit.Core; /// -/// Attribute that allows specifying a custom display name for a test method. +/// Attribute that allows specifying a custom display name for a test method or test class. /// /// /// -/// This attribute can be applied to test methods to provide more descriptive names than the default method name. +/// This attribute can be applied to test methods or test classes to provide more descriptive names than the default method or class name. /// /// /// The display name can include parameter placeholders in the format of "$parameterName" which will be -/// replaced with the actual parameter values during test execution. For example: +/// replaced with the actual parameter values during test execution. For test methods, method parameters +/// will be used for substitution. For test classes, constructor parameters will be used for substitution. For example: /// /// [Test] /// [Arguments("John", 25)] /// [DisplayName("User $name is $age years old")] /// public void TestUser(string name, int age) { ... } +/// +/// [Arguments("TestData")] +/// [DisplayName("Class with data: $data")] +/// public class MyTestClass(string data) { ... } /// /// /// -/// When this test runs, the display name would appear as "User John is 25 years old". +/// When these tests run, the display names would appear as "User John is 25 years old" and +/// "Class with data: TestData" respectively. /// /// /// /// The display name template. Can include parameter placeholders in the format of "$parameterName". +/// For methods, method parameter names can be referenced. For classes, constructor parameter names can be referenced. /// -[AttributeUsage(AttributeTargets.Method, Inherited = false)] +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] public sealed class DisplayNameAttribute(string displayName) : DisplayNameFormatterAttribute, IScopedAttribute { /// @@ -38,17 +45,34 @@ protected override string FormatDisplayName(DiscoveredTestContext context) var mutableDisplayName = displayName; - var parameters = testDetails + // Try to substitute method parameters first + var methodParameters = testDetails .MethodMetadata .Parameters .Zip(testDetails.TestMethodArguments, (parameterInfo, testArgument) => (ParameterInfo: parameterInfo, TestArgument: testArgument)); - foreach (var parameter in parameters) + foreach (var parameter in methodParameters) { mutableDisplayName = mutableDisplayName.Replace($"${parameter.ParameterInfo.Name}", ArgumentFormatter.Format(parameter.TestArgument, context.ArgumentDisplayFormatters)); } + // If there are still placeholders and we have class parameters, try to substitute them + if (mutableDisplayName.Contains('$') && testDetails.TestClassArguments.Length > 0) + { + var classParameters = testDetails + .MethodMetadata + .Class + .Parameters + .Zip(testDetails.TestClassArguments, (parameterInfo, testArgument) => (ParameterInfo: parameterInfo, TestArgument: testArgument)); + + foreach (var parameter in classParameters) + { + mutableDisplayName = mutableDisplayName.Replace($"${parameter.ParameterInfo.Name}", + ArgumentFormatter.Format(parameter.TestArgument, context.ArgumentDisplayFormatters)); + } + } + return mutableDisplayName; } } 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 7b7c4e0223..1ee2ef7ab6 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 @@ -572,7 +572,7 @@ namespace public DiscoveryResult() { } public static .DiscoveryResult Empty { get; } } - [(.Method, Inherited=false)] + [(.Class | .Method, Inherited=false)] public sealed class DisplayNameAttribute : .DisplayNameFormatterAttribute, .IScopedAttribute, .IScopedAttribute<.DisplayNameAttribute> { public DisplayNameAttribute(string displayName) { } 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 bde3a8bee3..e76b7d07fd 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 @@ -572,7 +572,7 @@ namespace public DiscoveryResult() { } public static .DiscoveryResult Empty { get; } } - [(.Method, Inherited=false)] + [(.Class | .Method, Inherited=false)] public sealed class DisplayNameAttribute : .DisplayNameFormatterAttribute, .IScopedAttribute, .IScopedAttribute<.DisplayNameAttribute> { public DisplayNameAttribute(string displayName) { } 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 20cf4713c9..3c88c659a6 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 @@ -526,7 +526,7 @@ namespace public DiscoveryResult() { } public static .DiscoveryResult Empty { get; } } - [(.Method, Inherited=false)] + [(.Class | .Method, Inherited=false)] public sealed class DisplayNameAttribute : .DisplayNameFormatterAttribute, .IScopedAttribute, .IScopedAttribute<.DisplayNameAttribute> { public DisplayNameAttribute(string displayName) { } diff --git a/TUnit.TestProject/ClassDisplayNameAttributeTests.cs b/TUnit.TestProject/ClassDisplayNameAttributeTests.cs new file mode 100644 index 0000000000..2641c0d281 --- /dev/null +++ b/TUnit.TestProject/ClassDisplayNameAttributeTests.cs @@ -0,0 +1,31 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject; + +[EngineTest(ExpectedResult.Pass)] +[DisplayName("Custom Class Display Name")] +public class ClassDisplayNameAttributeTests +{ + [Test] + public async Task Test() + { + // This test should inherit the class display name as a prefix or part of the test name + await Assert.That(TestContext.Current!.GetDisplayName()) + .DoesNotContain("ClassDisplayNameAttributeTests"); + } +} + +[EngineTest(ExpectedResult.Pass)] +[Arguments("TestValue")] +[DisplayName("Class with parameter: $value")] +public class ClassDisplayNameWithParametersTests(string value) +{ + [Test] + public async Task Test() + { + // This test should show the class display name with parameter substitution + var displayName = TestContext.Current!.GetDisplayName(); + await Assert.That(displayName) + .Contains("TestValue"); + } +} \ No newline at end of file