Skip to content

Commit 9d249f6

Browse files
spanglercojayaranigarg
authored andcommitted
Apply TestCategory from derived class on inherited test methods (#513)
* Apply TestCategory from derived class on inherited test methods Enables sharing test methods across a class hierarchy while categorizing the inherited tests based on the derived class that's actually running them. * Preserve DeclaringClassFullName from discovery to execution * Restore GetCustomAttributeForAssembly taking in a MemberInfo, but reference the member's Module instead of DeclaringType * Filter out duplicate test methods Selects the one declared closest to the test class among all valid test methods with the same name. * Fix compile after latest merge from master
1 parent 6c9b8e7 commit 9d249f6

File tree

13 files changed

+272
-28
lines changed

13 files changed

+272
-28
lines changed

src/Adapter/MSTest.CoreAdapter/Constants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ internal static class Constants
4040

4141
internal static readonly TestProperty TestClassNameProperty = TestProperty.Register("MSTestDiscoverer.TestClassName", TestClassNameLabel, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase));
4242

43+
internal static readonly TestProperty DeclaringClassNameProperty = TestProperty.Register("MSTestDiscoverer.DeclaringClassName", DeclaringClassNameLabel, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase));
44+
4345
internal static readonly TestProperty AsyncTestProperty = TestProperty.Register("MSTestDiscoverer.IsAsync", IsAsyncLabel, typeof(bool), TestPropertyAttributes.Hidden, typeof(TestCase));
4446

4547
#pragma warning disable CS0618 // Type or member is obsolete
@@ -98,6 +100,7 @@ internal static class Constants
98100
/// These Property names should not be localized.
99101
/// </summary>
100102
private const string TestClassNameLabel = "ClassName";
103+
private const string DeclaringClassNameLabel = "DeclaringClassName";
101104
private const string IsAsyncLabel = "IsAsync";
102105
private const string TestCategoryLabel = "TestCategory";
103106
private const string PriorityLabel = "Priority";

src/Adapter/MSTest.CoreAdapter/Discovery/TypeEnumerator.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ internal virtual ICollection<UnitTestElement> Enumerate(out ICollection<string>
7070
/// <returns> List of Valid Tests. </returns>
7171
internal Collection<UnitTestElement> GetTests(ICollection<string> warnings)
7272
{
73+
bool foundDuplicateTests = false;
74+
var foundTests = new HashSet<string>();
7375
var tests = new Collection<UnitTestElement>();
7476

7577
// Test class is already valid. Verify methods.
@@ -85,11 +87,35 @@ internal Collection<UnitTestElement> GetTests(ICollection<string> warnings)
8587

8688
if (this.testMethodValidator.IsValidTestMethod(method, this.type, warnings))
8789
{
90+
foundDuplicateTests = foundDuplicateTests || !foundTests.Add(method.Name);
8891
tests.Add(this.GetTestFromMethod(method, isMethodDeclaredInTestTypeAssembly, warnings));
8992
}
9093
}
9194

92-
return tests;
95+
if (!foundDuplicateTests)
96+
{
97+
return tests;
98+
}
99+
100+
// Remove duplicate test methods by taking the first one of each name
101+
// that is declared closest to the test class in the hierarchy.
102+
var inheritanceDepths = new Dictionary<string, int>();
103+
var currentType = this.type;
104+
int currentDepth = 0;
105+
106+
while (currentType != null)
107+
{
108+
inheritanceDepths[currentType.FullName] = currentDepth;
109+
++currentDepth;
110+
currentType = currentType.GetTypeInfo().BaseType;
111+
}
112+
113+
return new Collection<UnitTestElement>(
114+
tests.GroupBy(
115+
t => t.TestMethod.Name,
116+
(_, elements) =>
117+
elements.OrderBy(t => inheritanceDepths[t.TestMethod.DeclaringClassFullName ?? t.TestMethod.FullClassName]).First())
118+
.ToList());
93119
}
94120

95121
/// <summary>
@@ -126,9 +152,9 @@ internal UnitTestElement GetTestFromMethod(MethodInfo method, bool isDeclaredInT
126152
var asyncTypeName = method.GetAsyncTypeName();
127153
testElement.AsyncTypeName = asyncTypeName;
128154

129-
testElement.TestCategory = this.reflectHelper.GetCategories(method);
155+
testElement.TestCategory = this.reflectHelper.GetCategories(method, this.type);
130156

131-
testElement.DoNotParallelize = this.reflectHelper.IsDoNotParallelizeSet(method);
157+
testElement.DoNotParallelize = this.reflectHelper.IsDoNotParallelizeSet(method, this.type);
132158

133159
var traits = this.reflectHelper.GetTestPropertiesAsTraits(method);
134160

src/Adapter/MSTest.CoreAdapter/Execution/TypeCache.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -606,10 +606,26 @@ private TestMethodAttribute GetTestMethodAttribute(MethodInfo methodInfo, TestCl
606606
private MethodInfo GetMethodInfoForTestMethod(TestMethod testMethod, TestClassInfo testClassInfo)
607607
{
608608
var methodsInClass = testClassInfo.ClassType.GetRuntimeMethods().ToArray();
609+
MethodInfo testMethodInfo;
609610

610-
var testMethodInfo =
611-
methodsInClass.Where(method => method.Name.Equals(testMethod.Name))
612-
.FirstOrDefault(method => method.HasCorrectTestMethodSignature(true));
611+
if (testMethod.DeclaringClassFullName != null)
612+
{
613+
// Only find methods that match the given declaring name.
614+
testMethodInfo =
615+
methodsInClass.Where(method => method.Name.Equals(testMethod.Name)
616+
&& method.DeclaringType.FullName.Equals(testMethod.DeclaringClassFullName)
617+
&& method.HasCorrectTestMethodSignature(true)).FirstOrDefault();
618+
}
619+
else
620+
{
621+
// Either the declaring class is the same as the test class, or
622+
// the declaring class information wasn't passed in the test case.
623+
// Prioritize the former while maintaining previous behavior for the latter.
624+
var className = testClassInfo.ClassType.FullName;
625+
testMethodInfo =
626+
methodsInClass.Where(method => method.Name.Equals(testMethod.Name) && method.HasCorrectTestMethodSignature(true))
627+
.OrderByDescending(method => method.DeclaringType.FullName.Equals(className)).FirstOrDefault();
628+
}
613629

614630
// if correct method is not found, throw appropriate
615631
// exception about what is wrong.

src/Adapter/MSTest.CoreAdapter/Extensions/TestCaseExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ internal static UnitTestElement ToUnitTestElement(this TestCase testCase, string
2222
{
2323
var isAsync = (testCase.GetPropertyValue(Constants.AsyncTestProperty) as bool?) ?? false;
2424
var testClassName = testCase.GetPropertyValue(Constants.TestClassNameProperty) as string;
25+
var declaringClassName = testCase.GetPropertyValue(Constants.DeclaringClassNameProperty) as string;
2526

2627
TestMethod testMethod = new TestMethod(testCase.DisplayName, testClassName, source, isAsync);
2728

29+
if (declaringClassName != null && declaringClassName != testClassName)
30+
{
31+
testMethod.DeclaringClassFullName = declaringClassName;
32+
}
33+
2834
UnitTestElement testElement = new UnitTestElement(testMethod)
2935
{
3036
IsAsync = isAsync,

src/Adapter/MSTest.CoreAdapter/Helpers/ReflectHelper.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,11 @@ internal virtual bool IsMethodDeclaredInSameAssemblyAsType(MethodInfo method, Ty
315315
/// Get categories applied to the test method
316316
/// </summary>
317317
/// <param name="categoryAttributeProvider">The member to inspect.</param>
318+
/// <param name="owningType">The reflected type that owns <paramref name="categoryAttributeProvider"/>.</param>
318319
/// <returns>Categories defined.</returns>
319-
internal virtual string[] GetCategories(MemberInfo categoryAttributeProvider)
320+
internal virtual string[] GetCategories(MemberInfo categoryAttributeProvider, Type owningType)
320321
{
321-
var categories = this.GetCustomAttributesRecursively(categoryAttributeProvider, typeof(TestCategoryBaseAttribute));
322+
var categories = this.GetCustomAttributesRecursively(categoryAttributeProvider, owningType, typeof(TestCategoryBaseAttribute));
322323
List<string> testCategories = new List<string>();
323324

324325
if (categories != null)
@@ -346,11 +347,12 @@ internal ParallelizeAttribute GetParallelizeAttribute(Assembly assembly)
346347
/// Get the parallelization behavior for a test method.
347348
/// </summary>
348349
/// <param name="testMethod">Test method.</param>
350+
/// <param name="owningType">The type that owns <paramref name="testMethod"/>.</param>
349351
/// <returns>True if test method should not run in parallel.</returns>
350-
internal bool IsDoNotParallelizeSet(MemberInfo testMethod)
352+
internal bool IsDoNotParallelizeSet(MemberInfo testMethod, Type owningType)
351353
{
352354
return this.GetCustomAttributes(testMethod, typeof(DoNotParallelizeAttribute)).Any()
353-
|| this.GetCustomAttributes(testMethod.DeclaringType.GetTypeInfo(), typeof(DoNotParallelizeAttribute)).Any();
355+
|| this.GetCustomAttributes(owningType.GetTypeInfo(), typeof(DoNotParallelizeAttribute)).Any();
354356
}
355357

356358
/// <summary>
@@ -367,19 +369,20 @@ internal bool IsDoNotParallelizeSet(Assembly assembly)
367369
/// Gets custom attributes at the class and assembly for a method.
368370
/// </summary>
369371
/// <param name="attributeProvider">Method Info or Member Info or a Type</param>
372+
/// <param name="owningType">The type that owns <paramref name="attributeProvider"/>.</param>
370373
/// <param name="type"> What type of CustomAttribute you need. For instance: TestCategory, Owner etc.,</param>
371374
/// <returns>The categories of the specified type on the method. </returns>
372-
internal IEnumerable<object> GetCustomAttributesRecursively(MemberInfo attributeProvider, Type type)
375+
internal IEnumerable<object> GetCustomAttributesRecursively(MemberInfo attributeProvider, Type owningType, Type type)
373376
{
374377
var categories = this.GetCustomAttributes(attributeProvider, typeof(TestCategoryBaseAttribute));
375378
if (categories != null)
376379
{
377-
categories = categories.Concat(this.GetCustomAttributes(attributeProvider.DeclaringType.GetTypeInfo(), typeof(TestCategoryBaseAttribute))).ToArray();
380+
categories = categories.Concat(this.GetCustomAttributes(owningType.GetTypeInfo(), typeof(TestCategoryBaseAttribute))).ToArray();
378381
}
379382

380383
if (categories != null)
381384
{
382-
categories = categories.Concat(this.GetCustomAttributeForAssembly(attributeProvider, typeof(TestCategoryBaseAttribute))).ToArray();
385+
categories = categories.Concat(this.GetCustomAttributeForAssembly(owningType.GetTypeInfo(), typeof(TestCategoryBaseAttribute))).ToArray();
383386
}
384387

385388
if (categories != null)
@@ -401,8 +404,7 @@ internal virtual Attribute[] GetCustomAttributeForAssembly(MemberInfo memberInfo
401404
{
402405
return
403406
PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes(
404-
memberInfo.DeclaringType.GetTypeInfo().Assembly,
405-
type).OfType<Attribute>().ToArray();
407+
memberInfo.Module.Assembly, type).OfType<Attribute>().ToArray();
406408
}
407409

408410
/// <summary>

src/Adapter/MSTest.CoreAdapter/ObjectModel/TestMethod.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ public string DeclaringAssemblyName
7474
}
7575

7676
/// <summary>
77-
/// Gets or sets the declaring class full name. This will be used while getting navigation data.
77+
/// Gets or sets the declaring class full name.
78+
/// This will be used to resolve overloads and while getting navigation data.
7879
/// This will be null if FullClassName is same as DeclaringClassFullName.
7980
/// Reason to set to null in the above case is to minimise the transfer of data across appdomains and not have a perf hit.
8081
/// </summary>

src/Adapter/MSTest.CoreAdapter/ObjectModel/UnitTestElement.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ internal TestCase ToTestCase()
114114

115115
testCase.SetPropertyValue(TestAdapter.Constants.TestClassNameProperty, this.TestMethod.FullClassName);
116116

117+
// Set declaring type if present so the correct method info can be retrieved
118+
if (this.TestMethod.DeclaringClassFullName != null)
119+
{
120+
testCase.SetPropertyValue(TestAdapter.Constants.DeclaringClassNameProperty, this.TestMethod.DeclaringClassFullName);
121+
}
122+
117123
// Many of the tests will not be async, so there is no point in sending extra data
118124
if (this.IsAsync)
119125
{

src/Adapter/PlatformServices.Interface/ObjectModel/ITestMethod.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public interface ITestMethod
1919
string FullClassName { get; }
2020

2121
/// <summary>
22-
/// Gets the declaring class full name. This will be used while getting navigation data.
22+
/// Gets the declaring class full name.
23+
/// This will be used for resolving overloads and while getting navigation data.
2324
/// </summary>
2425
string DeclaringClassFullName { get; }
2526

test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Discovery/TypeEnumeratorTests.cs

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,82 @@ public void GetTestsShouldNotReturnBaseTestMethodsFromAnotherAssemblyByConfigura
183183
"DummyDerivedFromRemoteTestClass inherits DummyRemoteBaseTestClass from different assembly. BestTestMethod from DummyRemoteBaseTestClass should not be discovered when RunSettings MSTestV2 specifies EnableBaseClassTestMethodsFromOtherAssemblies = false.");
184184
}
185185

186+
[TestMethod]
187+
public void GetTestsShouldNotReturnHiddenTestMethods()
188+
{
189+
this.SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true, isMethodFromSameAssembly: true);
190+
TypeEnumerator typeEnumerator = this.GetTypeEnumeratorInstance(typeof(DummyHidingTestClass), Assembly.GetExecutingAssembly().FullName);
191+
192+
var tests = typeEnumerator.Enumerate(out this.warnings);
193+
194+
Assert.IsNotNull(tests);
195+
Assert.AreEqual(
196+
1,
197+
tests.Count(t => t.TestMethod.Name == "BaseTestMethod"),
198+
"DummyHidingTestClass declares BaseTestMethod directly so it should always be discovered.");
199+
Assert.AreEqual(
200+
1,
201+
tests.Count(t => t.TestMethod.Name == "DerivedTestMethod"),
202+
"DummyHidingTestClass declares BaseTestMethod directly so it should always be discovered.");
203+
Assert.IsFalse(
204+
tests.Any(t => t.TestMethod.DeclaringClassFullName == typeof(DummyBaseTestClass).FullName),
205+
"DummyHidingTestClass hides BaseTestMethod so declaring class should not be the base class");
206+
}
207+
208+
[TestMethod]
209+
public void GetTestsShouldReturnOverriddenTestMethods()
210+
{
211+
this.SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true, isMethodFromSameAssembly: true);
212+
TypeEnumerator typeEnumerator = this.GetTypeEnumeratorInstance(typeof(DummyOverridingTestClass), Assembly.GetExecutingAssembly().FullName);
213+
214+
var tests = typeEnumerator.Enumerate(out this.warnings);
215+
216+
Assert.IsNotNull(tests);
217+
Assert.AreEqual(
218+
1,
219+
tests.Count(t => t.TestMethod.Name == "BaseTestMethod"),
220+
"DummyOverridingTestClass inherits BaseTestMethod so it should be discovered.");
221+
Assert.AreEqual(
222+
1,
223+
tests.Count(t => t.TestMethod.Name == "DerivedTestMethod"),
224+
"DummyOverridingTestClass overrides DerivedTestMethod directly so it should always be discovered.");
225+
Assert.AreEqual(
226+
typeof(DummyHidingTestClass).FullName,
227+
tests.Single(t => t.TestMethod.Name == "BaseTestMethod").TestMethod.DeclaringClassFullName,
228+
"DummyOverridingTestClass inherits BaseTestMethod from DummyHidingTestClass specifically.");
229+
Assert.IsNull(
230+
tests.Single(t => t.TestMethod.Name == "DerivedTestMethod").TestMethod.DeclaringClassFullName,
231+
"DummyOverridingTestClass overrides DerivedTestMethod so is the declaring class.");
232+
}
233+
234+
[TestMethod]
235+
public void GetTestsShouldNotReturnHiddenTestMethodsFromAnyLevel()
236+
{
237+
this.SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true, isMethodFromSameAssembly: true);
238+
TypeEnumerator typeEnumerator = this.GetTypeEnumeratorInstance(typeof(DummySecondHidingTestClass), Assembly.GetExecutingAssembly().FullName);
239+
240+
var tests = typeEnumerator.Enumerate(out this.warnings);
241+
242+
Assert.IsNotNull(tests);
243+
Assert.AreEqual(
244+
1,
245+
tests.Count(t => t.TestMethod.Name == "BaseTestMethod"),
246+
"DummySecondHidingTestClass hides BaseTestMethod so it should be discovered.");
247+
Assert.AreEqual(
248+
1,
249+
tests.Count(t => t.TestMethod.Name == "DerivedTestMethod"),
250+
"DummySecondHidingTestClass hides DerivedTestMethod so it should be discovered.");
251+
Assert.IsFalse(
252+
tests.Any(t => t.TestMethod.DeclaringClassFullName == typeof(DummyBaseTestClass).FullName),
253+
"DummySecondHidingTestClass hides all base test methods so declaring class should not be any base class");
254+
Assert.IsFalse(
255+
tests.Any(t => t.TestMethod.DeclaringClassFullName == typeof(DummyHidingTestClass).FullName),
256+
"DummySecondHidingTestClass hides all base test methods so declaring class should not be any base class");
257+
Assert.IsFalse(
258+
tests.Any(t => t.TestMethod.DeclaringClassFullName == typeof(DummyOverridingTestClass).FullName),
259+
"DummySecondHidingTestClass hides all base test methods so declaring class should not be any base class");
260+
}
261+
186262
#endregion
187263

188264
#region GetTestFromMethod tests
@@ -247,7 +323,7 @@ public void GetTestFromMethodShouldSetTestCategory()
247323
var testCategories = new string[] { "foo", "bar" };
248324

249325
// Setup mocks
250-
this.mockReflectHelper.Setup(rh => rh.GetCategories(methodInfo)).Returns(testCategories);
326+
this.mockReflectHelper.Setup(rh => rh.GetCategories(methodInfo, typeof(DummyTestClass))).Returns(testCategories);
251327

252328
var testElement = typeEnumerator.GetTestFromMethod(methodInfo, true, this.warnings);
253329

@@ -496,5 +572,34 @@ public void DerivedTestMethod()
496572
}
497573
}
498574

575+
public class DummyHidingTestClass : DummyBaseTestClass
576+
{
577+
public new virtual void BaseTestMethod()
578+
{
579+
}
580+
581+
public virtual void DerivedTestMethod()
582+
{
583+
}
584+
}
585+
586+
public class DummyOverridingTestClass : DummyHidingTestClass
587+
{
588+
public override void DerivedTestMethod()
589+
{
590+
}
591+
}
592+
593+
public class DummySecondHidingTestClass : DummyOverridingTestClass
594+
{
595+
public new void BaseTestMethod()
596+
{
597+
}
598+
599+
public new void DerivedTestMethod()
600+
{
601+
}
602+
}
603+
499604
#endregion
500605
}

0 commit comments

Comments
 (0)