Skip to content
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

Support 'TestPropertyAttribute' on test classes #4249

Merged
merged 3 commits into from
Dec 10, 2024
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
11 changes: 8 additions & 3 deletions src/Adapter/MSTest.TestAdapter/Execution/TypeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -911,15 +911,20 @@ private TimeoutInfo GetTestTimeout(MethodInfo methodInfo, TestMethod testMethod)
/// </summary>
/// <param name="testMethodInfo"> The test Method Info. </param>
/// <param name="testContext"> The test Context. </param>
private static void SetCustomProperties(TestMethodInfo testMethodInfo, ITestContext testContext)
private void SetCustomProperties(TestMethodInfo testMethodInfo, ITestContext testContext)
{
DebugEx.Assert(testMethodInfo != null, "testMethodInfo is Null");
DebugEx.Assert(testMethodInfo.TestMethod != null, "testMethodInfo.TestMethod is Null");

object[] attributes = testMethodInfo.TestMethod.GetCustomAttributes(typeof(TestPropertyAttribute), false);
IEnumerable<TestPropertyAttribute> attributes = _reflectionHelper.GetDerivedAttributes<TestPropertyAttribute>(testMethodInfo.TestMethod, inherit: true);
DebugEx.Assert(attributes != null, "attributes is null");

foreach (TestPropertyAttribute attribute in attributes.Cast<TestPropertyAttribute>())
if (testMethodInfo.TestMethod.DeclaringType is { } testClass)
Evangelink marked this conversation as resolved.
Show resolved Hide resolved
{
attributes = attributes.Concat(_reflectionHelper.GetDerivedAttributes<TestPropertyAttribute>(testClass, inherit: true));
}

foreach (TestPropertyAttribute attribute in attributes)
{
if (!ValidateAndAssignTestProperty(testMethodInfo, testContext, attribute.Name, attribute.Value))
{
Expand Down
5 changes: 5 additions & 0 deletions src/Adapter/MSTest.TestAdapter/Helpers/ReflectHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,11 @@ internal virtual IEnumerable<Trait> GetTestPropertiesAsTraits(MemberInfo testPro
{
IEnumerable<TestPropertyAttribute> testPropertyAttributes = GetDerivedAttributes<TestPropertyAttribute>(testPropertyProvider, inherit: true);

if (testPropertyProvider.DeclaringType is { } testClass)
{
testPropertyAttributes = testPropertyAttributes.Concat(GetDerivedAttributes<TestPropertyAttribute>(testClass, inherit: true));
}

foreach (TestPropertyAttribute testProperty in testPropertyAttributes)
{
var testPropertyPair = new Trait(testProperty.Name, testProperty.Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting;
/// <summary>
/// The test property attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class TestPropertyAttribute : Attribute
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// 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.TestPlatform.MSTest.TestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Moq;

using TestFramework.ForTestingMSTest;

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Execution;

public class TestPropertyAttributeTests : TestContainer
{
private readonly TypeCache _typeCache;

public TestPropertyAttributeTests()
{
_typeCache = new TypeCache(new ReflectHelper());
var testablePlatformServiceProvider = new TestablePlatformServiceProvider();
testablePlatformServiceProvider.MockFileOperations.Setup(x => x.LoadAssembly(It.IsAny<string>(), It.IsAny<bool>())).Returns(GetType().Assembly);
PlatformServiceProvider.Instance = testablePlatformServiceProvider;

ReflectHelper.Instance.ClearCache();
}

protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
base.Dispose(disposing);
PlatformServiceProvider.Instance = null;
MSTestSettings.Reset();
}
}

#region GetTestMethodInfo tests

public void GetTestMethodInfoShouldAddPropertiesFromContainingClassCorrectly()
{
string className = typeof(DummyTestClassBase).FullName;
var testMethod = new TestMethod(nameof(DummyTestClassBase.VirtualTestMethodInBaseAndDerived), className, typeof(DummyTestClassBase).Assembly.GetName().Name, isAsync: false);

var testContext = new TestContextImplementation(testMethod, new StringWriter(), new Dictionary<string, object>());

_ = _typeCache.GetTestMethodInfo(
testMethod,
testContext,
false);

Assert.IsTrue(testContext.TryGetPropertyValue("TestMethodKeyFromBase", out object value1));
Assert.AreEqual("TestMethodValueFromBase", value1);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassBaseKey1", out object value2));
Assert.AreEqual("DummyTestClassBaseValue1", value2);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassBaseKey2", out object value3));
Assert.AreEqual("DummyTestClassBaseValue2", value3);

TestPlatform.ObjectModel.Trait[] traits = ReflectHelper.Instance.GetTestPropertiesAsTraits(typeof(DummyTestClassBase).GetMethod(nameof(DummyTestClassBase.VirtualTestMethodInBaseAndDerived))).ToArray();
Assert.AreEqual(3, traits.Length);
Assert.AreEqual("TestMethodKeyFromBase", traits[0].Name);
Assert.AreEqual("TestMethodValueFromBase", traits[0].Value);
Assert.AreEqual("DummyTestClassBaseKey1", traits[1].Name);
Assert.AreEqual("DummyTestClassBaseValue1", traits[1].Value);
Assert.AreEqual("DummyTestClassBaseKey2", traits[2].Name);
Assert.AreEqual("DummyTestClassBaseValue2", traits[2].Value);
}

public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClassesAndOverriddenMethodsCorrectly_OverriddenIsTestMethod()
{
string className = typeof(DummyTestClassDerived).FullName;
var testMethod = new TestMethod(nameof(DummyTestClassDerived.VirtualTestMethodInBaseAndDerived), className, typeof(DummyTestClassBase).Assembly.GetName().Name, isAsync: false);

var testContext = new TestContextImplementation(testMethod, new StringWriter(), new Dictionary<string, object>());

_ = _typeCache.GetTestMethodInfo(
testMethod,
testContext,
false);

Assert.IsTrue(testContext.TryGetPropertyValue("DerivedMethod1Key", out object value1));
Assert.AreEqual("DerivedMethod1Value", value1);

Assert.IsTrue(testContext.TryGetPropertyValue("TestMethodKeyFromBase", out object value2));
Assert.AreEqual("TestMethodValueFromBase", value2);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassDerivedKey1", out object value3));
Assert.AreEqual("DummyTestClassValue1", value3);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassDerivedKey2", out object value4));
Assert.AreEqual("DummyTestClassValue2", value4);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassBaseKey1", out object value5));
Assert.AreEqual("DummyTestClassBaseValue1", value5);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassBaseKey2", out object value6));
Assert.AreEqual("DummyTestClassBaseValue2", value6);

TestPlatform.ObjectModel.Trait[] traits = ReflectHelper.Instance.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInBaseAndDerived))).ToArray();
Assert.AreEqual(6, traits.Length);
Assert.AreEqual("DerivedMethod1Key", traits[0].Name);
Assert.AreEqual("DerivedMethod1Value", traits[0].Value);
Assert.AreEqual("TestMethodKeyFromBase", traits[1].Name);
Assert.AreEqual("TestMethodValueFromBase", traits[1].Value);
Assert.AreEqual("DummyTestClassDerivedKey1", traits[2].Name);
Assert.AreEqual("DummyTestClassValue1", traits[2].Value);
Assert.AreEqual("DummyTestClassDerivedKey2", traits[3].Name);
Assert.AreEqual("DummyTestClassValue2", traits[3].Value);
Assert.AreEqual("DummyTestClassBaseKey1", traits[4].Name);
Assert.AreEqual("DummyTestClassBaseValue1", traits[4].Value);
Assert.AreEqual("DummyTestClassBaseKey2", traits[5].Name);
Assert.AreEqual("DummyTestClassBaseValue2", traits[5].Value);
}

public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClassesAndOverriddenMethodsCorrectly_OverriddenIsNotTestMethod()
{
string className = typeof(DummyTestClassDerived).FullName;
var testMethod = new TestMethod(nameof(DummyTestClassDerived.VirtualTestMethodInDerivedButNotTestMethodInBase), className, typeof(DummyTestClassBase).Assembly.GetName().Name, isAsync: false);

var testContext = new TestContextImplementation(testMethod, new StringWriter(), new Dictionary<string, object>());

_ = _typeCache.GetTestMethodInfo(
testMethod,
testContext,
false);

Assert.IsTrue(testContext.TryGetPropertyValue("DerivedMethod2Key", out object value1));
Assert.AreEqual("DerivedMethod2Value", value1);

Assert.IsTrue(testContext.TryGetPropertyValue("NonTestMethodKeyFromBase", out object value2));
Assert.AreEqual("NonTestMethodValueFromBase", value2);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassDerivedKey1", out object value3));
Assert.AreEqual("DummyTestClassValue1", value3);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassDerivedKey2", out object value4));
Assert.AreEqual("DummyTestClassValue2", value4);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassBaseKey1", out object value5));
Assert.AreEqual("DummyTestClassBaseValue1", value5);

Assert.IsTrue(testContext.TryGetPropertyValue("DummyTestClassBaseKey2", out object value6));
Assert.AreEqual("DummyTestClassBaseValue2", value6);

TestPlatform.ObjectModel.Trait[] traits = ReflectHelper.Instance.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInDerivedButNotTestMethodInBase))).ToArray();
Assert.AreEqual(6, traits.Length);
Assert.AreEqual("DerivedMethod2Key", traits[0].Name);
Assert.AreEqual("DerivedMethod2Value", traits[0].Value);
Assert.AreEqual("NonTestMethodKeyFromBase", traits[1].Name);
Assert.AreEqual("NonTestMethodValueFromBase", traits[1].Value);
Assert.AreEqual("DummyTestClassDerivedKey1", traits[2].Name);
Assert.AreEqual("DummyTestClassValue1", traits[2].Value);
Assert.AreEqual("DummyTestClassDerivedKey2", traits[3].Name);
Assert.AreEqual("DummyTestClassValue2", traits[3].Value);
Assert.AreEqual("DummyTestClassBaseKey1", traits[4].Name);
Assert.AreEqual("DummyTestClassBaseValue1", traits[4].Value);
Assert.AreEqual("DummyTestClassBaseKey2", traits[5].Name);
Assert.AreEqual("DummyTestClassBaseValue2", traits[5].Value);
}

#endregion
#region dummy implementations

[TestClass]
[TestProperty("DummyTestClassBaseKey1", "DummyTestClassBaseValue1")]
[TestProperty("DummyTestClassBaseKey2", "DummyTestClassBaseValue2")]
internal class DummyTestClassBase
{
public TestContext TestContext { get; set; }

[TestMethod]
[TestProperty("TestMethodKeyFromBase", "TestMethodValueFromBase")]
public virtual void VirtualTestMethodInBaseAndDerived()
{
}

[TestProperty("NonTestMethodKeyFromBase", "NonTestMethodValueFromBase")]
public virtual void VirtualTestMethodInDerivedButNotTestMethodInBase()
{
}
}

[TestClass]
[TestProperty("DummyTestClassDerivedKey1", "DummyTestClassValue1")]
[TestProperty("DummyTestClassDerivedKey2", "DummyTestClassValue2")]
internal class DummyTestClassDerived : DummyTestClassBase
{
[TestProperty("DerivedMethod1Key", "DerivedMethod1Value")]
[TestMethod]
public override void VirtualTestMethodInBaseAndDerived() => base.VirtualTestMethodInBaseAndDerived();

[TestProperty("DerivedMethod2Key", "DerivedMethod2Value")]
[TestMethod]
public override void VirtualTestMethodInDerivedButNotTestMethodInBase() => base.VirtualTestMethodInDerivedButNotTestMethodInBase();
}

#endregion
}
25 changes: 20 additions & 5 deletions test/UnitTests/MSTestAdapter.UnitTests/Execution/TypeCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,9 @@ public void GetTestMethodInfoShouldReturnTestMethodInfoForMethodsAdornedWithADer

public void GetTestMethodInfoShouldSetTestContextWithCustomProperty()
{
// Not using _typeCache here which uses a mocked ReflectHelper which doesn't work well with this test.
// Setting up the mock feels unnecessary when the original production implementation can work just fine.
var typeCache = new TypeCache(new ReflectHelper());
Type type = typeof(DummyTestClassWithTestMethods);
MethodInfo methodInfo = type.GetMethod("TestMethodWithCustomProperty");
var testMethod = new TestMethod(methodInfo.Name, type.FullName, "A", isAsync: false);
Expand All @@ -1039,7 +1042,7 @@ public void GetTestMethodInfoShouldSetTestContextWithCustomProperty()
new ThreadSafeStringWriter(null, "test"),
new Dictionary<string, object>());

_typeCache.GetTestMethodInfo(testMethod, testContext, false);
typeCache.GetTestMethodInfo(testMethod, testContext, false);
KeyValuePair<string, object> customProperty = ((IDictionary<string, object>)testContext.Properties).FirstOrDefault(p => p.Key.Equals("WhoAmI", StringComparison.Ordinal));

Verify((object)customProperty is not null);
Expand All @@ -1048,6 +1051,9 @@ public void GetTestMethodInfoShouldSetTestContextWithCustomProperty()

public void GetTestMethodInfoShouldReportWarningIfCustomPropertyHasSameNameAsPredefinedProperties()
{
// Not using _typeCache here which uses a mocked ReflectHelper which doesn't work well with this test.
// Setting up the mock feels unnecessary when the original production implementation can work just fine.
var typeCache = new TypeCache(new ReflectHelper());
Type type = typeof(DummyTestClassWithTestMethods);
MethodInfo methodInfo = type.GetMethod("TestMethodWithOwnerAsCustomProperty");
var testMethod = new TestMethod(methodInfo.Name, type.FullName, "A", isAsync: false);
Expand All @@ -1056,7 +1062,7 @@ public void GetTestMethodInfoShouldReportWarningIfCustomPropertyHasSameNameAsPre
new ThreadSafeStringWriter(null, "test"),
new Dictionary<string, object>());

TestMethodInfo testMethodInfo = _typeCache.GetTestMethodInfo(testMethod, testContext, false);
TestMethodInfo testMethodInfo = typeCache.GetTestMethodInfo(testMethod, testContext, false);

Verify(testMethodInfo is not null);
string expectedMessage = string.Format(
Expand All @@ -1070,6 +1076,9 @@ public void GetTestMethodInfoShouldReportWarningIfCustomPropertyHasSameNameAsPre

public void GetTestMethodInfoShouldReportWarningIfCustomPropertyNameIsEmpty()
{
// Not using _typeCache here which uses a mocked ReflectHelper which doesn't work well with this test.
// Setting up the mock feels unnecessary when the original production implementation can work just fine.
var typeCache = new TypeCache(new ReflectHelper());
Type type = typeof(DummyTestClassWithTestMethods);
MethodInfo methodInfo = type.GetMethod("TestMethodWithEmptyCustomPropertyName");
var testMethod = new TestMethod(methodInfo.Name, type.FullName, "A", isAsync: false);
Expand All @@ -1078,7 +1087,7 @@ public void GetTestMethodInfoShouldReportWarningIfCustomPropertyNameIsEmpty()
new ThreadSafeStringWriter(null, "test"),
new Dictionary<string, object>());

TestMethodInfo testMethodInfo = _typeCache.GetTestMethodInfo(testMethod, testContext, false);
TestMethodInfo testMethodInfo = typeCache.GetTestMethodInfo(testMethod, testContext, false);

Verify(testMethodInfo is not null);
string expectedMessage = string.Format(
Expand All @@ -1091,6 +1100,9 @@ public void GetTestMethodInfoShouldReportWarningIfCustomPropertyNameIsEmpty()

public void GetTestMethodInfoShouldReportWarningIfCustomPropertyNameIsNull()
{
// Not using _typeCache here which uses a mocked ReflectHelper which doesn't work well with this test.
// Setting up the mock feels unnecessary when the original production implementation can work just fine.
var typeCache = new TypeCache(new ReflectHelper());
Type type = typeof(DummyTestClassWithTestMethods);
MethodInfo methodInfo = type.GetMethod("TestMethodWithNullCustomPropertyName");
var testMethod = new TestMethod(methodInfo.Name, type.FullName, "A", isAsync: false);
Expand All @@ -1099,7 +1111,7 @@ public void GetTestMethodInfoShouldReportWarningIfCustomPropertyNameIsNull()
new ThreadSafeStringWriter(null, "test"),
new Dictionary<string, object>());

TestMethodInfo testMethodInfo = _typeCache.GetTestMethodInfo(testMethod, testContext, false);
TestMethodInfo testMethodInfo = typeCache.GetTestMethodInfo(testMethod, testContext, false);

Verify(testMethodInfo is not null);
string expectedMessage = string.Format(
Expand All @@ -1112,6 +1124,9 @@ public void GetTestMethodInfoShouldReportWarningIfCustomPropertyNameIsNull()

public void GetTestMethodInfoShouldNotAddDuplicateTestPropertiesToTestContext()
{
// Not using _typeCache here which uses a mocked ReflectHelper which doesn't work well with this test.
// Setting up the mock feels unnecessary when the original production implementation can work just fine.
var typeCache = new TypeCache(new ReflectHelper());
Type type = typeof(DummyTestClassWithTestMethods);
MethodInfo methodInfo = type.GetMethod("TestMethodWithDuplicateCustomPropertyNames");
var testMethod = new TestMethod(methodInfo.Name, type.FullName, "A", isAsync: false);
Expand All @@ -1120,7 +1135,7 @@ public void GetTestMethodInfoShouldNotAddDuplicateTestPropertiesToTestContext()
new ThreadSafeStringWriter(null, "test"),
new Dictionary<string, object>());

TestMethodInfo testMethodInfo = _typeCache.GetTestMethodInfo(testMethod, testContext, false);
TestMethodInfo testMethodInfo = typeCache.GetTestMethodInfo(testMethod, testContext, false);

Verify(testMethodInfo is not null);

Expand Down