Skip to content

Commit 524db50

Browse files
Copilotbcollamore
andcommitted
Refactor PH2159 to target specific use case: single-parameter methods with multiple DataRows
Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
1 parent eaff89b commit 524db50

3 files changed

Lines changed: 108 additions & 49 deletions

File tree

Documentation/Diagnostics/PH2159.md

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,56 +12,49 @@
1212

1313
## Introduction
1414

15-
Test methods with multiple DataRow attributes may benefit from using combinatorial testing instead. Combinatorial testing allows you to test multiple combinations of parameter values more efficiently using the `Combinatorial.MSTest` package.
15+
Test methods with a single parameter and multiple DataRow attributes can be simplified using CombinatorialValues from the `Combinatorial.MSTest` package. This makes the test more maintainable and concise when you have many values to test for a single parameter.
1616

1717
## How to solve
1818

19-
Consider refactoring your test method to use combinatorial testing with the `Combinatorial.MSTest` package. This can make your tests more maintainable when you have many parameter combinations.
19+
Consider refactoring your test method to use CombinatorialValues with the `Combinatorial.MSTest` package. This is particularly beneficial for single-parameter methods with many different test values.
2020

2121
First, install the Combinatorial.MSTest package:
2222
```xml
2323
<PackageReference Include="Combinatorial.MSTest" Version="1.0.11" />
2424
```
2525

26-
Then refactor your test to use `CombinatorialValues` attributes.
26+
Then refactor your test to use `CombinatorialValues` attribute on the parameter.
2727

2828
## Example
2929

3030
Code that triggers a diagnostic:
3131
``` cs
3232
[TestMethod]
33-
[DataRow("private static readonly", false)]
34-
[DataRow("private static", false)]
35-
[DataRow("private const", false)]
36-
[DataRow("private", false)]
37-
[DataRow("public static readonly", true)]
38-
[DataRow("public const", true)]
39-
[DataRow("public static", true)]
40-
[DataRow("public readonly", true)]
41-
[DataRow("public", true)]
33+
[DataRow("private")]
34+
[DataRow("public")]
35+
[DataRow("internal")]
36+
[DataRow("protected")]
37+
[DataRow("static")]
38+
[DataRow("readonly")]
39+
[DataRow("const")]
4240
[TestCategory(TestDefinitions.UnitTests)]
43-
public void TestMethodWithManyDataRows(string modifiers, bool expected)
41+
public void TestMethodWithManyDataRows(string modifier)
4442
{
4543
// Test implementation
4644
}
4745
```
4846

49-
Potential refactored code using combinatorial testing:
47+
Refactored code using CombinatorialValues:
5048
``` cs
5149
[TestMethod]
5250
[TestCategory(TestDefinitions.UnitTests)]
5351
public void TestMethodWithCombinatorialValues(
54-
[CombinatorialValues("private", "public", "internal", "protected")] string visibility,
55-
[CombinatorialValues("", "static", "readonly", "const")] string modifier,
56-
[CombinatorialValues(true, false)] bool expected)
52+
[CombinatorialValues("private", "public", "internal", "protected", "static", "readonly", "const")] string modifier)
5753
{
58-
var modifiers = $"{visibility} {modifier}".Trim();
5954
// Test implementation
6055
}
6156
```
6257

63-
**Note**: The `Combinatorial.MSTest` package may not work with all parameter combinations and has limitations when mixing combinatorial and non-combinatorial parameters. Evaluate whether the refactoring is appropriate for your specific test case.
64-
6558
## Configuration
6659

6760
This analyzer is disabled by default. To enable it, add the following to your .editorconfig:
@@ -70,6 +63,9 @@ This analyzer is disabled by default. To enable it, add the following to your .e
7063
dotnet_diagnostic.PH2159.severity = suggestion
7164
```
7265

73-
The analyzer triggers when a test method has 4 or more DataRow attributes.
66+
The analyzer triggers when:
67+
- A test method has exactly 1 parameter
68+
- The method has 4 or more DataRow attributes
69+
- All DataRow attributes have exactly 1 argument each
7470

7571
The general ways of [suppressing](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings) diagnostics apply.

Philips.CodeAnalysis.MsTestAnalyzers/PreferCombinatorialTestingAnalyzer.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ namespace Philips.CodeAnalysis.MsTestAnalyzers
1212
public class PreferCombinatorialTestingAnalyzer : TestMethodDiagnosticAnalyzer
1313
{
1414
private const string Title = @"Consider using combinatorial testing";
15-
public const string MessageFormat = @"Consider using combinatorial testing instead of multiple DataRow attributes. This method has {0} DataRow attributes.";
16-
private const string Description = @"Methods with multiple DataRow attributes may benefit from combinatorial testing using the Combinatorial.MSTest package for better maintainability.";
15+
public const string MessageFormat = @"Consider using CombinatorialValues instead of {0} DataRow attributes for this single-parameter method.";
16+
private const string Description = @"Methods with a single parameter and multiple DataRow attributes can be simplified using CombinatorialValues from the Combinatorial.MSTest package.";
1717
private const string Category = Categories.MsTest;
1818

1919
private static readonly DiagnosticDescriptor Rule = new(DiagnosticId.PreferCombinatorialTestingOverDataRows.ToId(), Title, MessageFormat, Category, DiagnosticSeverity.Info, isEnabledByDefault: false, description: Description);
@@ -36,9 +36,16 @@ public PreferCombinatorialTestingImplementation(Helper helper, MsTestAttributeDe
3636

3737
protected override void OnTestMethod(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclaration, IMethodSymbol methodSymbol, bool isDataTestMethod)
3838
{
39-
// Count DataRow attributes
39+
// Only analyze methods with exactly 1 parameter
40+
if (methodDeclaration.ParameterList.Parameters.Count != 1)
41+
{
42+
return;
43+
}
44+
45+
// Count DataRow attributes and verify they all have exactly 1 argument
4046
SyntaxList<AttributeListSyntax> attributeLists = methodDeclaration.AttributeLists;
4147
var dataRowCount = 0;
48+
var allDataRowsHaveSingleArgument = true;
4249

4350
foreach (AttributeListSyntax attributeList in attributeLists)
4451
{
@@ -47,12 +54,18 @@ protected override void OnTestMethod(SyntaxNodeAnalysisContext context, MethodDe
4754
if (Helper.ForAttributes.IsDataRowAttribute(attribute, context))
4855
{
4956
dataRowCount++;
57+
58+
// Check if this DataRow has exactly 1 argument
59+
if (attribute.ArgumentList?.Arguments.Count != 1)
60+
{
61+
allDataRowsHaveSingleArgument = false;
62+
}
5063
}
5164
}
5265
}
5366

54-
// Only suggest if there are enough DataRows
55-
if (dataRowCount >= MinimumDataRowsForSuggestion)
67+
// Only suggest if there are enough DataRows with single arguments
68+
if (dataRowCount >= MinimumDataRowsForSuggestion && allDataRowsHaveSingleArgument)
5669
{
5770
Location location = methodDeclaration.Identifier.GetLocation();
5871
var diagnostic = Diagnostic.Create(Rule, location, dataRowCount);

Philips.CodeAnalysis.Test/MsTest/PreferCombinatorialTestingAnalyzerTest.cs

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class PreferCombinatorialTestingAnalyzerTest : DiagnosticVerifier
1717
{
1818
[TestMethod]
1919
[TestCategory(TestDefinitions.UnitTests)]
20-
public async Task PreferCombinatorialTestingTriggersDiagnosticWithMultipleDataRowsAsync()
20+
public async Task PreferCombinatorialTestingTriggersDiagnosticWithSingleParameterMethodAsync()
2121
{
2222
var testCode = @"
2323
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -26,12 +26,12 @@ public async Task PreferCombinatorialTestingTriggersDiagnosticWithMultipleDataRo
2626
public class TestClass
2727
{
2828
[TestMethod]
29-
[DataRow(""value1"", 1)]
30-
[DataRow(""value2"", 2)]
31-
[DataRow(""value3"", 3)]
32-
[DataRow(""value4"", 4)]
29+
[DataRow(""value1"")]
30+
[DataRow(""value2"")]
31+
[DataRow(""value3"")]
32+
[DataRow(""value4"")]
3333
[TestCategory(TestDefinitions.UnitTests)]
34-
public void TestMethodWithManyDataRows(string text, int number)
34+
public void TestMethodWithManyDataRows(string text)
3535
{
3636
// Test implementation
3737
}
@@ -41,7 +41,7 @@ public void TestMethodWithManyDataRows(string text, int number)
4141
DiagnosticResult expected = new()
4242
{
4343
Id = DiagnosticId.PreferCombinatorialTestingOverDataRows.ToId(),
44-
Message = new Regex(@"Consider using combinatorial testing instead of multiple DataRow attributes. This method has 4 DataRow attributes."),
44+
Message = new Regex(@"Consider using CombinatorialValues instead of 4 DataRow attributes for this single-parameter method."),
4545
Severity = DiagnosticSeverity.Info,
4646
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 13, 14) }
4747
};
@@ -51,7 +51,7 @@ public void TestMethodWithManyDataRows(string text, int number)
5151

5252
[TestMethod]
5353
[TestCategory(TestDefinitions.UnitTests)]
54-
public async Task PreferCombinatorialTestingNoDiagnosticWithFewDataRowsAsync()
54+
public async Task PreferCombinatorialTestingNoDiagnosticWithMultipleParametersAsync()
5555
{
5656
var testCode = @"
5757
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -63,8 +63,9 @@ public class TestClass
6363
[DataRow(""value1"", 1)]
6464
[DataRow(""value2"", 2)]
6565
[DataRow(""value3"", 3)]
66+
[DataRow(""value4"", 4)]
6667
[TestCategory(TestDefinitions.UnitTests)]
67-
public void TestMethodWithFewDataRows(string text, int number)
68+
public void TestMethodWithMultipleParameters(string text, int number)
6869
{
6970
// Test implementation
7071
}
@@ -98,7 +99,7 @@ public void TestMethodWithoutDataRows()
9899

99100
[TestMethod]
100101
[TestCategory(TestDefinitions.UnitTests)]
101-
public async Task PreferCombinatorialTestingTriggersDiagnosticWithManyDataRowsAsync()
102+
public async Task PreferCombinatorialTestingTriggersDiagnosticWithManySingleParameterDataRowsAsync()
102103
{
103104
var testCode = @"
104105
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -107,34 +108,83 @@ public async Task PreferCombinatorialTestingTriggersDiagnosticWithManyDataRowsAs
107108
public class TestClass
108109
{
109110
[TestMethod]
110-
[DataRow(""private static readonly"", false)]
111-
[DataRow(""private static"", false)]
112-
[DataRow(""private const"", false)]
113-
[DataRow(""private"", false)]
114-
[DataRow(""public static readonly"", true)]
115-
[DataRow(""public const"", true)]
116-
[DataRow(""public static"", true)]
117-
[DataRow(""public readonly"", true)]
118-
[DataRow(""public"", true)]
111+
[DataRow(""private"")]
112+
[DataRow(""public"")]
113+
[DataRow(""internal"")]
114+
[DataRow(""protected"")]
115+
[DataRow(""static"")]
116+
[DataRow(""readonly"")]
117+
[DataRow(""const"")]
119118
[TestCategory(TestDefinitions.UnitTests)]
120-
public void TestMethodWithManyDataRows(string modifiers, bool expected)
119+
public void TestMethodWithManyDataRows(string modifier)
121120
{
122-
// Test implementation similar to HelperTest example
121+
// Test implementation
123122
}
124123
}
125124
";
126125

127126
DiagnosticResult expected = new()
128127
{
129128
Id = DiagnosticId.PreferCombinatorialTestingOverDataRows.ToId(),
130-
Message = new Regex(@"Consider using combinatorial testing instead of multiple DataRow attributes. This method has 9 DataRow attributes."),
129+
Message = new Regex(@"Consider using CombinatorialValues instead of 7 DataRow attributes for this single-parameter method."),
131130
Severity = DiagnosticSeverity.Info,
132-
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 18, 14) }
131+
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 16, 14) }
133132
};
134133

135134
await VerifyDiagnostic(testCode, expected).ConfigureAwait(false);
136135
}
137136

137+
[TestMethod]
138+
[TestCategory(TestDefinitions.UnitTests)]
139+
public async Task PreferCombinatorialTestingNoDiagnosticWithFewSingleParameterDataRowsAsync()
140+
{
141+
var testCode = @"
142+
using Microsoft.VisualStudio.TestTools.UnitTesting;
143+
144+
[TestClass]
145+
public class TestClass
146+
{
147+
[TestMethod]
148+
[DataRow(""value1"")]
149+
[DataRow(""value2"")]
150+
[DataRow(""value3"")]
151+
[TestCategory(TestDefinitions.UnitTests)]
152+
public void TestMethodWithFewDataRows(string text)
153+
{
154+
// Test implementation
155+
}
156+
}
157+
";
158+
159+
await VerifySuccessfulCompilation(testCode).ConfigureAwait(false);
160+
}
161+
162+
[TestMethod]
163+
[TestCategory(TestDefinitions.UnitTests)]
164+
public async Task PreferCombinatorialTestingNoDiagnosticWithDataRowsHavingMultipleArgumentsAsync()
165+
{
166+
var testCode = @"
167+
using Microsoft.VisualStudio.TestTools.UnitTesting;
168+
169+
[TestClass]
170+
public class TestClass
171+
{
172+
[TestMethod]
173+
[DataRow(""value1"", 1)] // These DataRows have multiple arguments
174+
[DataRow(""value2"", 2)] // but method has single parameter
175+
[DataRow(""value3"", 3)]
176+
[DataRow(""value4"", 4)]
177+
[TestCategory(TestDefinitions.UnitTests)]
178+
public void TestMethodWithMismatchedDataRows(string text)
179+
{
180+
// Test implementation
181+
}
182+
}
183+
";
184+
185+
await VerifySuccessfulCompilation(testCode).ConfigureAwait(false);
186+
}
187+
138188
protected override DiagnosticAnalyzer GetDiagnosticAnalyzer()
139189
{
140190
return new PreferCombinatorialTestingAnalyzer();

0 commit comments

Comments
 (0)