-
Notifications
You must be signed in to change notification settings - Fork 279
Add TestCategories property to ITestDataRow for per-test-case categorization #5795
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?
Changes from all commits
6bd59e3
e15fd0d
dfa519e
8211671
64510f6
0ae57ea
9a54dcf
41270af
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,112 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
// EXAMPLE: Using TestCategories with TestDataRow | ||
// This demonstrates the new TestCategories functionality added to ITestDataRow | ||
|
||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
||
namespace ExampleUsage; | ||
|
||
[TestClass] | ||
public class TestCategoriesExampleTests | ||
{ | ||
/// <summary> | ||
/// Example showing how to use TestCategories with TestDataRow for dynamic data. | ||
/// Each test case can now have its own categories. | ||
/// </summary> | ||
[TestMethod] | ||
[DynamicData(nameof(GetTestDataWithCategories), DynamicDataSourceType.Method)] | ||
public void ExampleTestWithDynamicCategories(string value, string expectedResult) | ||
{ | ||
// Test logic here | ||
Assert.AreEqual(expectedResult, ProcessValue(value)); | ||
} | ||
|
||
/// <summary> | ||
/// Example showing how TestCategories from TestDataRow merge with method-level categories. | ||
/// This test method has "MethodLevel" category, and individual test cases add their own. | ||
/// </summary> | ||
[TestCategory("MethodLevel")] | ||
[TestMethod] | ||
[DynamicData(nameof(GetTestDataWithMergedCategories), DynamicDataSourceType.Method)] | ||
public void ExampleTestWithMergedCategories(string value) | ||
{ | ||
// Test logic here | ||
Assert.IsNotNull(ProcessValue(value)); | ||
} | ||
|
||
/// <summary> | ||
/// Test data source that demonstrates TestCategories usage. | ||
/// </summary> | ||
public static IEnumerable<object[]> GetTestDataWithCategories() | ||
{ | ||
// Fast unit test case | ||
yield return new TestDataRow<(string, string)>(("input1", "output1")) | ||
{ | ||
TestCategories = new List<string> { "Unit", "Fast" }, | ||
DisplayName = "Fast unit test case" | ||
}; | ||
|
||
// Slow integration test case | ||
yield return new TestDataRow<(string, string)>(("input2", "output2")) | ||
{ | ||
TestCategories = new List<string> { "Integration", "Slow", "Database" }, | ||
DisplayName = "Integration test with database" | ||
}; | ||
|
||
// Performance test case | ||
yield return new TestDataRow<(string, string)>(("input3", "output3")) | ||
{ | ||
TestCategories = new List<string> { "Performance", "Load" }, | ||
DisplayName = "Load testing scenario" | ||
}; | ||
|
||
// Regular test case without specific categories | ||
yield return new TestDataRow<(string, string)>(("input4", "output4")) | ||
{ | ||
DisplayName = "Standard test case" | ||
// No TestCategories specified - will inherit any from method/class/assembly level | ||
}; | ||
|
||
// Traditional data row still works | ||
yield return new object[] { "input5", "output5" }; | ||
} | ||
|
||
/// <summary> | ||
/// Test data that will merge with method-level categories. | ||
/// </summary> | ||
public static IEnumerable<object[]> GetTestDataWithMergedCategories() | ||
{ | ||
// This will have both "MethodLevel" (from method attribute) and "DataLevel" categories | ||
yield return new TestDataRow<string>("test_value") | ||
{ | ||
TestCategories = new List<string> { "DataLevel", "Specific" }, | ||
DisplayName = "Test with combined categories" | ||
}; | ||
} | ||
|
||
private static string ProcessValue(string input) => $"processed_{input}"; | ||
} | ||
|
||
/* | ||
* Usage scenarios enabled by this feature: | ||
* | ||
* 1. FILTERING BY CATEGORY: | ||
* - Can now run tests filtered by categories applied to individual test cases | ||
* - Example: dotnet test --filter "TestCategory=Fast" will run only fast test cases | ||
* - Example: dotnet test --filter "TestCategory=Integration" will run only integration test cases | ||
* | ||
* 2. MIXED SCENARIOS: | ||
* - Can have different types of test cases (unit, integration, performance) in same test method | ||
* - Each test case can have appropriate categories for filtering/organization | ||
* | ||
* 3. CATEGORY INHERITANCE: | ||
* - Test cases inherit categories from method/class/assembly level | ||
* - TestDataRow categories are merged with existing categories (no overriding) | ||
* | ||
* 4. BACKWARD COMPATIBILITY: | ||
* - Existing TestDataRow usage continues to work unchanged | ||
* - TestCategories property is optional (nullable) | ||
* - Regular object[] data rows continue to work | ||
*/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -439,14 +439,26 @@ | |
foreach (object?[] dataOrTestDataRow in data) | ||
{ | ||
object?[] d = dataOrTestDataRow; | ||
if (TestDataSourceHelpers.TryHandleITestDataRow(d, methodInfo.GetParameters(), out d, out string? ignoreMessageFromTestDataRow, out string? displayNameFromTestDataRow)) | ||
if (TestDataSourceHelpers.TryHandleITestDataRow(d, methodInfo.GetParameters(), out d, out string? ignoreMessageFromTestDataRow, out string? displayNameFromTestDataRow, out IList<string>? testCategoriesFromTestDataRow)) | ||
{ | ||
testDataSourceIgnoreMessage = ignoreMessageFromTestDataRow ?? testDataSourceIgnoreMessage; | ||
} | ||
|
||
UnitTestElement discoveredTest = test.Clone(); | ||
discoveredTest.DisplayName = displayNameFromTestDataRow ?? dataSource.GetDisplayName(methodInfo, d) ?? discoveredTest.DisplayName; | ||
|
||
// Merge test categories from the test data row with the existing categories | ||
if (testCategoriesFromTestDataRow is { Count: > 0 }) | ||
{ | ||
string[] existingCategories = discoveredTest.TestCategory ?? []; | ||
var mergedCategories = new HashSet<string>(existingCategories); | ||
foreach (string category in testCategoriesFromTestDataRow) | ||
{ | ||
mergedCategories.Add(category); | ||
} | ||
Check failure on line 458 in src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs
|
||
discoveredTest.TestCategory = [.. mergedCategories]; | ||
} | ||
|
||
// If strategy is DisplayName and we have a duplicate test name don't expand the test, bail out. | ||
#pragma warning disable CS0618 // Type or member is obsolete | ||
if (test.TestMethod.TestIdGenerationStrategy == TestIdGenerationStrategy.DisplayName | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,6 +40,12 @@ public TestDataRow(T value) | |
[DataMember] | ||
public string? DisplayName { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the test categories for the test case. | ||
/// </summary> | ||
[DataMember] | ||
public IList<string>? TestCategories { get; set; } | ||
Evangelink marked this conversation as resolved.
Show resolved
Hide resolved
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. @Youssef1313 what about using a readonly collection here? We don't really expect to modify the collection from our end. The benefit of keeping it this way is to allow users to create the 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. I kept |
||
|
||
/// <inheritdoc cref="Value"/> | ||
object? ITestDataRow.Value => Value; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using Microsoft.MSTestV2.CLIAutomation; | ||
|
||
namespace MSTest.IntegrationTests; | ||
|
||
public class TestCategoriesFromTestDataRowTests : CLITestBase | ||
{ | ||
private const string TestAssetName = "TestCategoriesFromTestDataRowProject"; | ||
|
||
[TestMethod] | ||
public async Task TestCategoriesFromTestDataRow_ShouldDiscoverTestsWithIntegrationCategory() | ||
{ | ||
// Arrange | ||
string assemblyPath = GetAssetFullPath(TestAssetName); | ||
|
||
// Act - Filter by "Integration" category which should come from TestDataRow | ||
System.Collections.Immutable.ImmutableArray<Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase> testCases = DiscoverTests(assemblyPath, "TestCategory~Integration"); | ||
|
||
// Assert - Should discover the test that has Integration category from TestDataRow | ||
Assert.AreEqual(1, testCases.Length, "Should discover exactly 1 test with Integration category"); | ||
|
||
var integrationTest = testCases[0]; | ||
Assert.IsTrue(integrationTest.DisplayName.Contains("Integration and Slow"), "Should be the test with Integration and Slow categories"); | ||
} | ||
|
||
[TestMethod] | ||
public async Task TestCategoriesFromTestDataRow_ShouldDiscoverTestsWithUnitCategory() | ||
{ | ||
// Arrange | ||
string assemblyPath = GetAssetFullPath(TestAssetName); | ||
|
||
// Act - Filter by "Unit" category which should come from TestDataRow | ||
System.Collections.Immutable.ImmutableArray<Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase> testCases = DiscoverTests(assemblyPath, "TestCategory~Unit"); | ||
|
||
// Assert - Should discover the test that has Unit category from TestDataRow | ||
Assert.AreEqual(1, testCases.Length, "Should discover exactly 1 test with Unit category"); | ||
|
||
var unitTest = testCases[0]; | ||
Assert.IsTrue(unitTest.DisplayName.Contains("Unit and Fast"), "Should be the test with Unit and Fast categories"); | ||
} | ||
|
||
[TestMethod] | ||
public async Task TestCategoriesFromTestDataRow_ShouldCombineMethodAndDataCategories() | ||
{ | ||
// Arrange | ||
string assemblyPath = GetAssetFullPath(TestAssetName); | ||
|
||
// Act - Filter by "MethodLevel" category (method attribute) | ||
System.Collections.Immutable.ImmutableArray<Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase> methodLevelTests = DiscoverTests(assemblyPath, "TestCategory~MethodLevel"); | ||
|
||
// Act - Filter by "DataLevel" category (from TestDataRow) | ||
System.Collections.Immutable.ImmutableArray<Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase> dataLevelTests = DiscoverTests(assemblyPath, "TestCategory~DataLevel"); | ||
|
||
// Assert - The same test should be found by both filters since categories are merged | ||
Assert.AreEqual(1, methodLevelTests.Length, "Should discover exactly 1 test with MethodLevel category"); | ||
Assert.AreEqual(1, dataLevelTests.Length, "Should discover exactly 1 test with DataLevel category"); | ||
|
||
var methodTest = methodLevelTests[0]; | ||
var dataTest = dataLevelTests[0]; | ||
|
||
Assert.AreEqual(methodTest.FullyQualifiedName, dataTest.FullyQualifiedName, "Both filters should find the same test"); | ||
Assert.IsTrue(methodTest.DisplayName.Contains("method and data categories"), "Should be the test with combined categories"); | ||
} | ||
|
||
[TestMethod] | ||
public async Task TestCategoriesFromTestDataRow_ShouldExecuteCorrectly() | ||
{ | ||
// Arrange | ||
string assemblyPath = GetAssetFullPath(TestAssetName); | ||
|
||
// Act | ||
System.Collections.Immutable.ImmutableArray<Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase> testCases = DiscoverTests(assemblyPath, "FullyQualifiedName~TestCategoriesFromTestDataRowTests"); | ||
System.Collections.Immutable.ImmutableArray<Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult> testResults = await RunTestsAsync(testCases); | ||
|
||
// Assert - All tests should pass | ||
VerifyE2E.TestsPassed( | ||
testResults, | ||
"TestMethodWithDynamicDataCategories (Test with Integration and Slow categories)", | ||
"TestMethodWithDynamicDataCategories (Test with Unit and Fast categories)", | ||
"TestMethodWithDynamicDataCategories (Test with no additional categories)", | ||
"TestMethodWithDynamicDataCategories (value4,4)", | ||
"TestMethodWithMethodLevelCategoriesAndDataCategories (Test with method and data categories)"); | ||
|
||
VerifyE2E.FailedTestCount(testResults, 0); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFrameworks>net6.0;net462</TargetFrameworks> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="MSTest.TestFramework" Version="*" /> | ||
</ItemGroup> | ||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
||
namespace TestCategoriesFromTestDataRowProject; | ||
|
||
[TestClass] | ||
public class TestCategoriesFromTestDataRowTests | ||
{ | ||
[TestMethod] | ||
[DynamicData(nameof(GetTestDataWithCategories), DynamicDataSourceType.Method)] | ||
public void TestMethodWithDynamicDataCategories(string value, int number) | ||
{ | ||
Assert.IsTrue(!string.IsNullOrEmpty(value)); | ||
Assert.IsTrue(number > 0); | ||
} | ||
|
||
public static IEnumerable<object[]> GetTestDataWithCategories() | ||
{ | ||
// Test data row with categories | ||
yield return new TestDataRow<(string, int)>(("value1", 1)) | ||
{ | ||
TestCategories = new List<string> { "Integration", "Slow" }, | ||
DisplayName = "Test with Integration and Slow categories" | ||
}; | ||
|
||
// Test data row with different categories | ||
yield return new TestDataRow<(string, int)>(("value2", 2)) | ||
{ | ||
TestCategories = new List<string> { "Unit", "Fast" }, | ||
DisplayName = "Test with Unit and Fast categories" | ||
}; | ||
|
||
// Test data row with no categories (should inherit from method level) | ||
yield return new TestDataRow<(string, int)>(("value3", 3)) | ||
{ | ||
DisplayName = "Test with no additional categories" | ||
}; | ||
|
||
// Regular data row (not TestDataRow) - should work as before | ||
yield return new object[] { "value4", 4 }; | ||
} | ||
|
||
[TestCategory("MethodLevel")] | ||
[TestMethod] | ||
[DynamicData(nameof(GetTestDataWithCategoriesForMethodWithCategory), DynamicDataSourceType.Method)] | ||
public void TestMethodWithMethodLevelCategoriesAndDataCategories(string value) | ||
{ | ||
Assert.IsTrue(!string.IsNullOrEmpty(value)); | ||
} | ||
|
||
public static IEnumerable<object[]> GetTestDataWithCategoriesForMethodWithCategory() | ||
{ | ||
// This should have both "MethodLevel" and "DataLevel" categories | ||
yield return new TestDataRow<string>("test") | ||
{ | ||
TestCategories = new List<string> { "DataLevel" }, | ||
DisplayName = "Test with method and data categories" | ||
}; | ||
} | ||
} |
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 please add a blank line between the closing
}
and this line.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.
Added proper blank line spacing between the closing brace and the following comment (41270af)