Skip to content

Commit

Permalink
Added TestDataSource and DynamicData attributes (microsoft#195)
Browse files Browse the repository at this point in the history
* Dynamic Data attribute implementation.

* Added E2E tests.

* Added more tests.

* PR feedback.

* Added sealed for DynamicDataAttribute.

* Changed TestDataSource to ITestDataSource.

* Fixed failing tests.
  • Loading branch information
harshjain2 authored Jun 14, 2017
1 parent bf5fb4f commit 50c26d1
Show file tree
Hide file tree
Showing 35 changed files with 891 additions and 61 deletions.
29 changes: 28 additions & 1 deletion src/TestFramework/MSTest.Core/Attributes/DataRowAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 4,15 @@
namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;

/// <summary>
/// Attribute to define inline data for a test method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class DataRowAttribute : Attribute
public class DataRowAttribute : Attribute, ITestDataSource
{
/// <summary>
/// Initializes a new instance of the <see cref="DataRowAttribute"/> class.
Expand Down Expand Up @@ -48,5 51,29 @@ public DataRowAttribute(object data1, params object[] moreData)
/// Gets or sets display name in test results for customization.
/// </summary>
public string DisplayName { get; set; }

/// <inheritdoc />
public IEnumerable<object[]> GetData(MethodInfo methodInfo)
{
return new[] { this.Data };
}

/// <inheritdoc />
public string GetDisplayName(MethodInfo methodInfo, object[] data)
{
if (!string.IsNullOrWhiteSpace(this.DisplayName))
{
return this.DisplayName;
}
else
{
if (data != null)
{
return string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DataDrivenResultDisplayName, methodInfo.Name, string.Join(",", data));
}
}

return null;
}
}
}
30 changes: 13 additions & 17 deletions src/TestFramework/MSTest.Core/Attributes/DataTestMethodAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 5,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

/// <summary>
/// Attribute for data driven test where data can be specified inline.
Expand All @@ -24,40 24,36 @@ public class DataTestMethodAttribute : TestMethodAttribute
/// </returns>
public override TestResult[] Execute(ITestMethod testMethod)
{
DataRowAttribute[] dataRows = testMethod.GetAttributes<DataRowAttribute>(false);
ITestDataSource[] dataSources = testMethod.GetAttributes<Attribute>(true)?.Where(a => a is ITestDataSource).OfType<ITestDataSource>().ToArray();

if (dataRows == null || dataRows.Length == 0)
if (dataSources == null || dataSources.Length == 0)
{
return new TestResult[] { new TestResult() { Outcome = UnitTestOutcome.Failed, TestFailureException = new Exception(FrameworkMessages.NoDataRow) } };
}

return RunDataDrivenTest(testMethod, dataRows);
return RunDataDrivenTest(testMethod, dataSources);
}

/// <summary>
/// Run data driven test method.
/// </summary>
/// <param name="testMethod"> Test method to execute. </param>
/// <param name="dataRows"> Data Row. </param>
/// <param name="testDataSources">Test data sources. </param>
/// <returns> Results of execution. </returns>
internal static TestResult[] RunDataDrivenTest(ITestMethod testMethod, DataRowAttribute[] dataRows)
internal static TestResult[] RunDataDrivenTest(ITestMethod testMethod, ITestDataSource[] testDataSources)
{
List<TestResult> results = new List<TestResult>();

foreach (var dataRow in dataRows)
foreach (var testDataSource in testDataSources)
{
TestResult result = testMethod.Invoke(dataRow.Data);

if (!string.IsNullOrEmpty(dataRow.DisplayName))
{
result.DisplayName = dataRow.DisplayName;
}
else
foreach (var data in testDataSource.GetData(testMethod.MethodInfo))
{
result.DisplayName = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DataDrivenResultDisplayName, testMethod.TestMethodName, string.Join(",", dataRow.Data));
}
TestResult result = testMethod.Invoke(data);

result.DisplayName = testDataSource.GetDisplayName(testMethod.MethodInfo, data);

results.Add(result);
results.Add(result);
}
}

return results.ToArray();
Expand Down
143 changes: 143 additions & 0 deletions src/TestFramework/MSTest.Core/Attributes/DynamicDataAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,143 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestTools.UnitTesting.Attributes
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Enum to specify whether the data is stored as property or in method.
/// </summary>
public enum DynamicDataSourceType
{
/// <summary>
/// Data is declared as property.
/// </summary>
Property = 0,

/// <summary>
/// Data is declared in method.
/// </summary>
Method = 1
}

/// <summary>
/// Attribute to define dynamic data for a test method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class DynamicDataAttribute : Attribute, ITestDataSource
{
private string dynamicDataSourceName;

private Type dynamicDataDeclaringType;

private DynamicDataSourceType dynamicDataSourceType;

/// <summary>
/// Initializes a new instance of the <see cref="DynamicDataAttribute"/> class.
/// </summary>
/// <param name="dynamicDataSourceName">
/// The name of method or property having test data.
/// </param>
/// <param name="dynamicDataSourceType">
/// Specifies whether the data is stored as property or in method.
/// </param>
public DynamicDataAttribute(string dynamicDataSourceName, DynamicDataSourceType dynamicDataSourceType = DynamicDataSourceType.Property)
{
this.dynamicDataSourceName = dynamicDataSourceName;
this.dynamicDataSourceType = dynamicDataSourceType;
}

/// <summary>
/// Initializes a new instance of the <see cref="DynamicDataAttribute"/> class.
/// </summary>
/// <param name="dynamicDataSourceName">
/// The type in which the test data is declared (as property or in method).
/// </param>
/// <param name="dynamicDataDeclaringType">
/// The declaring type of property or method having data.
/// </param>
/// <param name="dynamicDataSourceType">
/// Specifies whether the data is stored as property or in method.
/// </param>
public DynamicDataAttribute(string dynamicDataSourceName, Type dynamicDataDeclaringType, DynamicDataSourceType dynamicDataSourceType = DynamicDataSourceType.Property)
: this(dynamicDataSourceName, dynamicDataSourceType)
{
this.dynamicDataDeclaringType = dynamicDataDeclaringType;
}

/// <inheritdoc />
public IEnumerable<object[]> GetData(MethodInfo methodInfo)
{
// Check if the declaring type of test data is passed in constructor. If not, default to test method's class type.
if (this.dynamicDataDeclaringType == null)
{
this.dynamicDataDeclaringType = methodInfo.DeclaringType;
}

object obj = null;

switch (this.dynamicDataSourceType)
{
case DynamicDataSourceType.Property:
var property = this.dynamicDataDeclaringType.GetTypeInfo().GetDeclaredProperty(this.dynamicDataSourceName);
if (property == null)
{
throw new ArgumentNullException(string.Format("{0} {1}", DynamicDataSourceType.Property, this.dynamicDataSourceName));
}

obj = property.GetValue(null, null);

break;

case DynamicDataSourceType.Method:
var method = this.dynamicDataDeclaringType.GetTypeInfo().GetDeclaredMethod(this.dynamicDataSourceName);
if (method == null)
{
throw new ArgumentNullException(string.Format("{0} {1}", DynamicDataSourceType.Method, this.dynamicDataSourceName));
}

obj = method.Invoke(null, null);

break;
}

if (obj == null)
{
throw new ArgumentNullException(
string.Format(
FrameworkMessages.DynamicDataValueNull,
this.dynamicDataSourceName,
this.dynamicDataDeclaringType.FullName));
}

IEnumerable<object[]> enumerable = obj as IEnumerable<object[]>;
if (enumerable == null)
{
throw new ArgumentNullException(
string.Format(
FrameworkMessages.DynamicDataIEnumerableNull,
this.dynamicDataSourceName,
this.dynamicDataDeclaringType.FullName));
}

return enumerable;
}

/// <inheritdoc />
public string GetDisplayName(MethodInfo methodInfo, object[] data)
{
if (data != null)
{
return string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DataDrivenResultDisplayName, methodInfo.Name, string.Join(",", data));
}

return null;
}
}
}
13 changes: 7 additions & 6 deletions src/TestFramework/MSTest.Core/Attributes/VSTestAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 7,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;

#pragma warning disable SA1402 // FileMayOnlyContainASingleType
#pragma warning disable SA1649 // SA1649FileNameMustMatchTypeName
Expand All @@ -15,7 16,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting
/// Enumeration for timeouts, that can be used with the <see cref="TimeoutAttribute"/> class.
/// The type of the enumeration must match
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue", Justification ="Compat reasons")]
[SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue", Justification = "Compat reasons")]
public enum TestTimeout
{
/// <summary>
Expand Down Expand Up @@ -57,14 58,14 @@ public class TestMethodAttribute : Attribute
/// <remarks>Extensions can override this method to customize running a TestMethod.</remarks>
public virtual TestResult[] Execute(ITestMethod testMethod)
{
DataRowAttribute[] dataRows = testMethod.GetAttributes<DataRowAttribute>(false);
ITestDataSource[] dataSources = testMethod.GetAttributes<Attribute>(true)?.Where(a => a is ITestDataSource).OfType<ITestDataSource>().ToArray();

if (dataRows == null || dataRows.Length == 0)
if (dataSources == null || dataSources.Length == 0)
{
return new TestResult[] { testMethod.Invoke(null) };
}

return DataTestMethodAttribute.RunDataDrivenTest(testMethod, dataRows);
return DataTestMethodAttribute.RunDataDrivenTest(testMethod, dataSources);
}
}

Expand Down Expand Up @@ -438,7 439,7 @@ public TestResult()
/// [DataSource("Provider=SQLOLEDB.1;Data Source=source;Integrated Security=SSPI;Initial Catalog=EqtCoverage;Persist Security Info=False", "MyTable")]
/// [DataSource("dataSourceNameFromConfigFile")]
/// </example>
[SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification ="Compat")]
[SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "Compat")]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class DataSourceAttribute : Attribute
{
Expand All @@ -448,7 449,7 @@ public sealed class DataSourceAttribute : Attribute
/// <summary>
/// The default provider name for DataSource.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate", Justification ="Compat")]
[SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate", Justification = "Compat")]
public static readonly string DefaultProviderName = "System.Data.OleDb";

/// <summary>
Expand Down
41 changes: 41 additions & 0 deletions src/TestFramework/MSTest.Core/Interfaces/ITestDataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;

/// <summary>
/// Test data source for data driven tests.
/// </summary>
public interface ITestDataSource
{
/// <summary>
/// Gets the test data from custom test data source.
/// </summary>
/// <param name="methodInfo">
/// The method info of test method.
/// </param>
/// <returns>
/// Test data for calling test method.
/// </returns>
IEnumerable<object[]> GetData(MethodInfo methodInfo);

/// <summary>
/// Gets the display name corresponding to test data row for displaying in TestResults.
/// </summary>
/// <param name="methodInfo">
/// The method Info of test method.
/// </param>
/// <param name="data">
/// The test data which is passed to test method.
/// </param>
/// <returns>
/// The <see cref="string"/>.
/// </returns>
string GetDisplayName(MethodInfo methodInfo, object[] data);
}
}
3 changes: 3 additions & 0 deletions src/TestFramework/MSTest.Core/MSTest.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 35,8 @@
<LocDocumentationSubPath>Core</LocDocumentationSubPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="Attributes\DynamicDataAttribute.cs" />
<Compile Include="Interfaces\ITestDataSource.cs" />
<Compile Include="Interfaces\ITestMethod.cs" />
<Compile Include="Internal\Helper.cs" />
<Compile Include="DataAccessMethod.cs" />
Expand Down Expand Up @@ -72,6 74,7 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>FrameworkMessages.Designer.cs</LastGenOutput>
<CustomToolNamespace>Microsoft.VisualStudio.TestTools.UnitTesting</CustomToolNamespace>
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(TestFxRoot)scripts\build\TestFx.targets" />
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 50c26d1

Please sign in to comment.