I needed this myself and I wrote a new class PropertyAutoData
which combines PropertyData
and AutoFixture similar to how InlineAutoData
combines InlineData
and AutoFixture. The usage is:
[Theory]
[PropertyAutoData("ColorPairs")]
public void ReverseColors([TestCaseParameter] TestData testData, int autoGenValue) { ... }
public static IEnumerable<object[]> ColorPairs
{
get
{
yield return new object[] { new TestData { Input = Color.Black, Expected = Color.White } };
yield return new object[] { new TestData { Input = Color.White, Expected = Color.Black } };
}
}
Note the [TestCaseParameter]
attribute on param testData
. This indicates that the parameter value will be supplied from the property. It needs to be explicitly specified because AutoFixture kind of changed the meaning of parametrized tests.
Running this yields 2 tests as expected in which the autoGenValue
has the same auto-generated value. You can change this behavior by setting the Scope
of auto-generated data:
[PropertyAutoData("ColorPairs", Scope = AutoDataScope.Test)] // default is TestCase
You can also use this together with SubSpec's Thesis
:
[Thesis]
[PropertyAutoData("ColorPairs")]
public void ReverseColors([TestCaseParameter] TestData testData, int autoGenValue)
To use this with Moq you need to extend it, i.e.
public class PropertyMockAutoDataAttribute : PropertyAutoDataAttribute
{
public PropertyFakeAutoDataAttribute(string propertyName)
: base(propertyName, new Fixture().Customize(new AutoMoqCustomization()))
{
}
}
Here's the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Ploeh.AutoFixture.Xunit;
using Xunit.Extensions;
/// <summary>
/// Provides a data source for a data theory, with the data coming from a public static property on the test class combined with auto-generated data specimens generated by AutoFixture.
/// </summary>
public class PropertyAutoDataAttribute : AutoDataAttribute
{
private readonly string _propertyName;
public PropertyAutoDataAttribute(string propertyName)
{
_propertyName = propertyName;
}
public PropertyAutoDataAttribute(string propertyName, IFixture fixture)
: base(fixture)
{
_propertyName = propertyName;
}
/// <summary>
/// Gets or sets the scope of auto-generated data.
/// </summary>
public AutoDataScope Scope { get; set; }
public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
{
var parameters = methodUnderTest.GetParameters();
var testCaseParametersIndices = GetTestCaseParameterIndices(parameters);
if (!testCaseParametersIndices.Any())
{
throw new InvalidOperationException(string.Format("There are no parameters marked using {0}.", typeof(TestCaseParameterAttribute).Name));
}
if (testCaseParametersIndices.Length == parameters.Length)
{
throw new InvalidOperationException(string.Format("All parameters are provided by the property. Do not use {0} unless there are other parameters that AutoFixture should provide.", typeof(PropertyDataAttribute).Name));
}
// 'split' the method under test in 2 methods: one to get the test case data sets and another one to get the auto-generated data set
var testCaseParameterTypes = parameterTypes.Where((t, i) => testCaseParametersIndices.Contains(i)).ToArray();
var testCaseMethod = CreateDynamicMethod(methodUnderTest.Name + "_TestCase", testCaseParameterTypes);
var autoFixtureParameterTypes = parameterTypes.Where((t, i) => !testCaseParametersIndices.Contains(i)).ToArray();
var autoFixtureTestMethod = CreateDynamicMethod(methodUnderTest.Name + "_AutoFixture", autoFixtureParameterTypes);
// merge the test case data and the auto-generated data into a new array and yield it
// the merge depends on the Scope:
// * if the scope is TestCase then auto-generate data once for all tests
// * if the scope is Test then auto-generate data for every test
var testCaseDataSets = GetTestCaseDataSets(methodUnderTest.DeclaringType, testCaseMethod, testCaseParameterTypes);
object[] autoGeneratedDataSet = null;
if (Scope == AutoDataScope.TestCase)
{
autoGeneratedDataSet = GetAutoGeneratedData(autoFixtureTestMethod, autoFixtureParameterTypes);
}
var autoFixtureParameterIndices = Enumerable.Range(0, parameters.Length).Except(testCaseParametersIndices).ToArray();
foreach (var testCaseDataSet in testCaseDataSets)
{
if (testCaseDataSet.Length != testCaseParameterTypes.Length)
{
throw new ApplicationException("There is a mismatch between the values generated by the property and the test case parameters.");
}
var mergedDataSet = new object[parameters.Length];
CopyAtIndices(testCaseDataSet, mergedDataSet, testCaseParametersIndices);
if (Scope == AutoDataScope.Test)
{
autoGeneratedDataSet = GetAutoGeneratedData(autoFixtureTestMethod, autoFixtureParameterTypes);
}
CopyAtIndices(autoGeneratedDataSet, mergedDataSet, autoFixtureParameterIndices);
yield return mergedDataSet;
}
}
private static int[] GetTestCaseParameterIndices(ParameterInfo[] parameters)
{
var testCaseParametersIndices = new List<int>();
for (var index = 0; index < parameters.Length; index++)
{
var parameter = parameters[index];
var isTestCaseParameter = parameter.GetCustomAttributes(typeof(TestCaseParameterAttribute), false).Length > 0;
if (isTestCaseParameter)
{
testCaseParametersIndices.Add(index);
}
}
return testCaseParametersIndices.ToArray();
}
private static MethodInfo CreateDynamicMethod(string name, Type[] parameterTypes)
{
var method = new DynamicMethod(name, typeof(void), parameterTypes);
return method.GetBaseDefinition();
}
private object[] GetAutoGeneratedData(MethodInfo method, Type[] parameterTypes)
{
var autoDataSets = base.GetData(method, parameterTypes).ToArray();
if (autoDataSets == null || autoDataSets.Length == 0)
{
throw new ApplicationException("There was no data automatically generated by AutoFixture");
}
if (autoDataSets.Length != 1)
{
throw new ApplicationException("Multiple sets of data were automatically generated. Only one was expected.");
}
return autoDataSets.Single();
}
private IEnumerable<object[]> GetTestCaseDataSets(Type testClassType, MethodInfo method, Type[] parameterTypes)
{
var attribute = new PropertyDataAttribute(_propertyName) { PropertyType = testClassType };
return attribute.GetData(method, parameterTypes);
}
private static void CopyAtIndices(object[] source, object[] target, int[] indices)
{
var sourceIndex = 0;
foreach (var index in indices)
{
target[index] = source[sourceIndex++];
}
}
}
/// <summary>
/// Defines the scope of auto-generated data in a theory.
/// </summary>
public enum AutoDataScope
{
/// <summary>
/// Data is auto-generated only once for all tests.
/// </summary>
TestCase,
/// <summary>
/// Data is auto-generated for every test.
/// </summary>
Test
}
/// <summary>
/// Indicates that the parameter is part of a test case rather than being auto-generated by AutoFixture.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class TestCaseParameterAttribute : Attribute
{
}