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
38 changes: 31 additions & 7 deletions TUnit.Core/Attributes/TestMetadata/DisplayNameAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,37 @@
namespace TUnit.Core;

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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:
/// <code>
/// [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) { ... }
/// </code>
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <param name="displayName">
/// 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.
/// </param>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
public sealed class DisplayNameAttribute(string displayName) : DisplayNameFormatterAttribute, IScopedAttribute<DisplayNameAttribute>
{
/// <inheritdoc />
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down
31 changes: 31 additions & 0 deletions TUnit.TestProject/ClassDisplayNameAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading