-
Notifications
You must be signed in to change notification settings - Fork 14
feat: Add MsTest analyzer to suggest DisplayName and Description attributes for commented tests #841
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Add MsTest analyzer to suggest DisplayName and Description attributes for commented tests #841
Changes from 7 commits
8c6eaa8
8ebe896
e87072b
1b0af01
809ae19
fac9da7
025dae2
c8ace77
5ad28b3
2113104
d900363
a7cb058
e88eb83
47725f9
f3d313f
4288bf9
eaed4cd
57956d8
3fdc09d
cb7d7a6
7022935
c256631
d818605
a6c4257
cbb1f61
b900f87
0962cb6
bf46025
360bd05
799f0f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # PH2146: Test should use DisplayName or Description attribute instead of comments | ||
|
|
||
| | Property | Value | | ||
| |--|--| | ||
| | Package | [Philips.CodeAnalysis.MsTestAnalyzers](https://www.nuget.org/packages/Philips.CodeAnalysis.MsTestAnalyzers) | | ||
| | Diagnostic ID | PH2146 | | ||
| | Category | [MsTest](../MsTest.md) | | ||
| | Analyzer | [TestShouldUseDisplayNameAnalyzer](https://github.com/philips-software/roslyn-analyzers/blob/main/Philips.CodeAnalysis.MsTestAnalyzers/TestShouldUseDisplayNameAnalyzer.cs) | ||
| | CodeFix | No | | ||
| | Severity | Info | | ||
| | Enabled By Default | Yes | | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enabled By Default: No.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated documentation to show "Enabled By Default: No" in PH2146.md. Commit f3d313f addresses this. |
||
|
|
||
| ## Introduction | ||
|
|
||
| Test methods and DataRow attributes often contain inline comments that describe their purpose, but these comments are not visible in test runners. MSTest provides better alternatives: `Description` attribute for test methods and `DisplayName` parameter for DataRow attributes. | ||
|
|
||
| ## How to solve | ||
|
|
||
| For test methods with meaningful comments, add a `Description` attribute instead of using inline comments. | ||
|
|
||
| For DataRow attributes with meaningful comments, add a `DisplayName` parameter instead of using inline comments. | ||
|
|
||
| ## Example | ||
|
|
||
| Code that triggers a diagnostic: | ||
| ``` cs | ||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| // This test verifies that addition works correctly | ||
| [TestMethod] | ||
| public void TestAddition() { } | ||
|
|
||
| [DataTestMethod] | ||
| [DataRow(1, 2)] // Should add positive numbers | ||
| [DataRow(-1, 1)] // Should handle negative numbers | ||
| public void TestAdditionWithData(int a, int b) { } | ||
| } | ||
| ``` | ||
|
|
||
| And the replacement code: | ||
| ``` cs | ||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| [TestMethod] | ||
| [Description("This test verifies that addition works correctly")] | ||
| public void TestAddition() { } | ||
|
|
||
| [DataTestMethod] | ||
| [DataRow(1, 2, DisplayName = "Should add positive numbers")] | ||
| [DataRow(-1, 1, DisplayName = "Should handle negative numbers")] | ||
| public void TestAdditionWithData(int a, int b) { } | ||
| } | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| This analyzer does not offer any special configuration. The general ways of [suppressing](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings) diagnostics apply. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| // © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. | ||
|
|
||
| using System.Collections.Immutable; | ||
| using System.Linq; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
| using Philips.CodeAnalysis.Common; | ||
|
|
||
| namespace Philips.CodeAnalysis.MsTestAnalyzers | ||
| { | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public class TestShouldUseDisplayNameAnalyzer : TestMethodDiagnosticAnalyzer | ||
| { | ||
| private const string Title = @"Test should use DisplayName or Description attribute instead of comments"; | ||
| public const string MessageFormat = @"Consider using DisplayName parameter for DataRow or Description attribute for test method instead of inline comments"; | ||
| private const string Description = @"Using DisplayName parameter for DataRow attributes or Description attribute for test methods makes test purpose more visible in test runners and provides better documentation."; | ||
| private const string Category = Categories.MsTest; | ||
|
|
||
| private static readonly DiagnosticDescriptor Rule = new(DiagnosticId.UseDisplayNameOrDescription.ToId(), Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. enabled by default: false
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Set |
||
|
|
||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | ||
|
|
||
| protected override TestMethodImplementation OnInitializeTestMethodAnalyzer(AnalyzerOptions options, Compilation compilation, MsTestAttributeDefinitions definitions) | ||
| { | ||
| return new TestShouldUseDisplayName(definitions, Helper); | ||
| } | ||
|
|
||
| public class TestShouldUseDisplayName : TestMethodImplementation | ||
| { | ||
| public TestShouldUseDisplayName(MsTestAttributeDefinitions definitions, Helper helper) : base(definitions, helper) | ||
| { } | ||
|
|
||
| protected override void OnTestMethod(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclaration, IMethodSymbol methodSymbol, bool isDataTestMethod) | ||
| { | ||
| if (isDataTestMethod) | ||
| { | ||
| CheckDataTestMethodForDisplayName(context, methodDeclaration); | ||
| } | ||
| else | ||
| { | ||
| CheckTestMethodForDescription(context, methodDeclaration); | ||
| } | ||
| } | ||
|
|
||
| private void CheckDataTestMethodForDisplayName(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclaration) | ||
| { | ||
| // Check for DataRow attributes with comments but no DisplayName | ||
| foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists) | ||
| { | ||
| foreach (AttributeSyntax attribute in attributeList.Attributes) | ||
|
Check warning on line 52 in Philips.CodeAnalysis.MsTestAnalyzers/TestShouldUseDisplayNameAnalyzer.cs
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sonar suggests this: "Loops should be simplified using the "Where" LINQ method"
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Simplified the nested foreach loops using LINQ SelectMany and Where methods as suggested. The code is now more concise and follows functional programming best practices. Commit cbb1f61. |
||
| { | ||
| if (Helper.ForAttributes.IsDataRowAttribute(attribute, context)) | ||
| { | ||
| // Check if this DataRow has a comment but no DisplayName | ||
| var hasDisplayName = attribute.ArgumentList?.Arguments.Any(arg => | ||
| arg.NameEquals?.Name.Identifier.ValueText == "DisplayName") == true; | ||
|
|
||
| if (!hasDisplayName) | ||
| { | ||
| // Look for trailing comment on the same line | ||
| var comment = GetTrailingComment(attribute); | ||
| if (!string.IsNullOrWhiteSpace(comment)) | ||
| { | ||
| var diagnostic = Diagnostic.Create(Rule, attribute.GetLocation()); | ||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void CheckTestMethodForDescription(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclaration) | ||
| { | ||
| // Check if test method has Description attribute | ||
| var hasDescription = Helper.ForAttributes.HasAttribute(methodDeclaration.AttributeLists, context, MsTestFrameworkDefinitions.DescriptionAttribute, out _, out _); | ||
|
|
||
| if (!hasDescription) | ||
| { | ||
| // Look for leading comment before the method | ||
| var comment = GetLeadingComment(methodDeclaration); | ||
| if (!string.IsNullOrWhiteSpace(comment)) | ||
| { | ||
| var diagnostic = Diagnostic.Create(Rule, methodDeclaration.Identifier.GetLocation()); | ||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private string GetTrailingComment(AttributeSyntax attribute) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest to rename this method to include
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed the methods to |
||
| { | ||
| SyntaxToken token = attribute.GetLastToken(); | ||
| SyntaxTrivia trivia = token.TrailingTrivia.FirstOrDefault(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia)); | ||
|
|
||
| if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) | ||
| { | ||
| return ExtractCommentText(trivia.ToString()); | ||
| } | ||
|
|
||
| return string.Empty; | ||
| } | ||
|
|
||
| private string GetLeadingComment(MethodDeclarationSyntax methodDeclaration) | ||
| { | ||
| SyntaxTriviaList leadingTrivia = methodDeclaration.GetLeadingTrivia(); | ||
|
|
||
| // Look for single-line comment immediately before the method | ||
| SyntaxTrivia comment = leadingTrivia.LastOrDefault(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia)); | ||
| if (comment.IsKind(SyntaxKind.SingleLineCommentTrivia)) | ||
| { | ||
| return ExtractCommentText(comment.ToString()); | ||
| } | ||
|
|
||
| return string.Empty; | ||
| } | ||
|
|
||
| private string ExtractCommentText(string commentTrivia) | ||
|
Check warning on line 119 in Philips.CodeAnalysis.MsTestAnalyzers/TestShouldUseDisplayNameAnalyzer.cs
|
||
| { | ||
| if (string.IsNullOrWhiteSpace(commentTrivia)) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| // Remove // and trim whitespace | ||
| var text = commentTrivia.Trim(); | ||
| if (text.StartsWith("//")) | ||
| { | ||
| text = text.Substring(2).Trim(); | ||
| } | ||
|
|
||
| // Only consider meaningful comments (more than just a few characters) | ||
| if (text.Length > 5) | ||
| { | ||
| return text; | ||
| } | ||
|
|
||
| return string.Empty; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| // © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. | ||
|
|
||
| using System.Threading.Tasks; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
| using Philips.CodeAnalysis.Common; | ||
| using Philips.CodeAnalysis.MsTestAnalyzers; | ||
| using Philips.CodeAnalysis.Test.Helpers; | ||
| using Philips.CodeAnalysis.Test.Verifiers; | ||
|
|
||
| namespace Philips.CodeAnalysis.Test.MsTest | ||
| { | ||
| [TestClass] | ||
| public class TestShouldUseDisplayNameAnalyzerTest : DiagnosticVerifier | ||
| { | ||
| protected override DiagnosticAnalyzer GetDiagnosticAnalyzer() | ||
| { | ||
| return new TestShouldUseDisplayNameAnalyzer(); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [TestCategory(TestDefinitions.UnitTests)] | ||
| public async Task DataRowWithCommentButNoDisplayNameShouldTriggerDiagnostic() | ||
| { | ||
| const string testCode = @" | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| [DataTestMethod] | ||
| [DataRow(1, 2)] // Should add numbers correctly | ||
| public void TestAddition(int a, int b) { } | ||
| }"; | ||
|
|
||
| await VerifyDiagnostic(testCode, DiagnosticId.UseDisplayNameOrDescription).ConfigureAwait(false); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [TestCategory(TestDefinitions.UnitTests)] | ||
| public async Task DataRowWithDisplayNameShouldNotTriggerDiagnostic() | ||
| { | ||
| const string testCode = @" | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| [DataTestMethod] | ||
| [DataRow(1, 2, DisplayName = ""Should add numbers correctly"")] | ||
| public void TestAddition(int a, int b) { } | ||
| }"; | ||
|
|
||
| await VerifySuccessfulCompilation(testCode).ConfigureAwait(false); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [TestCategory(TestDefinitions.UnitTests)] | ||
| public async Task DataRowWithoutCommentShouldNotTriggerDiagnostic() | ||
| { | ||
| const string testCode = @" | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| [DataTestMethod] | ||
| [DataRow(1, 2)] | ||
| public void TestAddition(int a, int b) { } | ||
| }"; | ||
|
|
||
| await VerifySuccessfulCompilation(testCode).ConfigureAwait(false); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [TestCategory(TestDefinitions.UnitTests)] | ||
| public async Task TestMethodWithCommentButNoDescriptionShouldTriggerDiagnostic() | ||
|
Check failure on line 77 in Philips.CodeAnalysis.Test/MsTest/TestShouldUseDisplayNameAnalyzerTest.cs
|
||
| { | ||
| const string testCode = @" | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| // This test verifies that addition works correctly | ||
| [TestMethod] | ||
| public void TestAddition() { } | ||
| }"; | ||
|
|
||
| await VerifyDiagnostic(testCode, DiagnosticId.UseDisplayNameOrDescription).ConfigureAwait(false); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [TestCategory(TestDefinitions.UnitTests)] | ||
| public async Task TestMethodWithDescriptionShouldNotTriggerDiagnostic() | ||
|
Check failure on line 95 in Philips.CodeAnalysis.Test/MsTest/TestShouldUseDisplayNameAnalyzerTest.cs
|
||
| { | ||
| const string testCode = @" | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| [TestMethod] | ||
| [Description(""This test verifies that addition works correctly"")] | ||
| public void TestAddition() { } | ||
| }"; | ||
|
|
||
| await VerifySuccessfulCompilation(testCode).ConfigureAwait(false); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [TestCategory(TestDefinitions.UnitTests)] | ||
| public async Task TestMethodWithoutCommentShouldNotTriggerDiagnostic() | ||
|
Check failure on line 113 in Philips.CodeAnalysis.Test/MsTest/TestShouldUseDisplayNameAnalyzerTest.cs
|
||
| { | ||
| const string testCode = @" | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| [TestMethod] | ||
| public void TestAddition() { } | ||
| }"; | ||
|
|
||
| await VerifySuccessfulCompilation(testCode).ConfigureAwait(false); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [TestCategory(TestDefinitions.UnitTests)] | ||
| public async Task TestMethodWithShortCommentShouldNotTriggerDiagnostic() | ||
|
Check failure on line 130 in Philips.CodeAnalysis.Test/MsTest/TestShouldUseDisplayNameAnalyzerTest.cs
|
||
| { | ||
| const string testCode = @" | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| // Test | ||
| [TestMethod] | ||
| public void TestAddition() { } | ||
| }"; | ||
|
|
||
| await VerifySuccessfulCompilation(testCode).ConfigureAwait(false); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [TestCategory(TestDefinitions.UnitTests)] | ||
| public async Task TestMethodWithBoilerplateCommentShouldNotTriggerDiagnostic() | ||
|
Check failure on line 148 in Philips.CodeAnalysis.Test/MsTest/TestShouldUseDisplayNameAnalyzerTest.cs
|
||
| { | ||
| const string testCode = @" | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| // TODO: implement this test | ||
| [TestMethod] | ||
| public void TestAddition() { } | ||
| }"; | ||
|
|
||
| await VerifySuccessfulCompilation(testCode).ConfigureAwait(false); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [TestCategory(TestDefinitions.UnitTests)] | ||
| public async Task MultipleDataRowsWithCommentsShouldTriggerDiagnostics() | ||
| { | ||
| const string testCode = @" | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| [DataTestMethod] | ||
| [DataRow(1, 2)] // Should add positive numbers | ||
| [DataRow(0, 0)] // Should handle zero values | ||
| public void TestAddition(int a, int b) { } | ||
| }"; | ||
|
|
||
| await VerifyDiagnostic(testCode, DiagnosticId.UseDisplayNameOrDescription).ConfigureAwait(false); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to change PH2146 to PH2150. (PH2146 has been claimed by other analyzers now.) Update file name, file contents, DiagnosticId, etc. Copilot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated diagnostic ID from PH2146 to PH2150 as requested. Changes include:
Commit 8c7c8f9 implements these changes.