I'm trying to create AutoPropertyDataAttribute
based on CompositeDataAttribute
from this example AutoFixture: PropertyData and heterogeneous parameters.
It works with single set of parameters, but fails with more sets of parameters. Here is code:
public static IEnumerable<object[]> NumericSequence
{
get
{
yield return new object[] {1};
//yield return new object[] {2};
}
}
[Theory]
[AutoPropertyData("NumericSequence")]
public void Test(int? p1, int? p2, int? p3)
{
Assert.NotNull(p1);
Assert.NotNull(p2);
}
public class AutoPropertyDataAttribute : CompositeDataAttribute
{
public AutoPropertyDataAttribute(string propertyName)
: base(
new DataAttribute[] {
new PropertyDataAttribute(propertyName),
new AutoDataAttribute()
})
{
}
}
Trying to uncomment the second yield
will break test with message:
System.InvalidOperationException: Expected 2 parameters, got 1 parameters
at Ploeh.AutoFixture.Xunit.CompositeDataAttribute.<GetData>d__0.MoveNext()
at Xunit.Extensions.TheoryAttribute.<GetData>d__7.MoveNext()
at Xunit.Extensions.TheoryAttribute.EnumerateTestCommands(IMethodInfo method)
Same happens with ClassDataAttribute
I ran into this issue and decided to implement a custom DataAttribute to solve the problem. I couldn't use either attribute as a base class (reasons below) so I just took the things I needed from the source of each. Thank you OSS :)
Things to note:
PropertyDataAttribute
as a base classAutoDataAttribute
as a base classGist
Or inline source below:
public class AutoPropertyDataAttribute : DataAttribute
{
private readonly string _propertyName;
private readonly Func<IFixture> _createFixture;
public AutoPropertyDataAttribute(string propertyName)
: this(propertyName, () => new Fixture())
{ }
protected AutoPropertyDataAttribute(string propertyName, Func<IFixture> createFixture)
{
_propertyName = propertyName;
_createFixture = createFixture;
}
public Type PropertyHost { get; set; }
private IEnumerable<object[]> GetAllParameterObjects(MethodInfo methodUnderTest)
{
var type = PropertyHost ?? methodUnderTest.DeclaringType;
var property = type.GetProperty(_propertyName, BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy);
if (property == null)
throw new ArgumentException(string.Format("Could not find public static property {0} on {1}", _propertyName, type.FullName));
var obj = property.GetValue(null, null);
if (obj == null)
return null;
var enumerable = obj as IEnumerable<object[]>;
if (enumerable != null)
return enumerable;
var singleEnumerable = obj as IEnumerable<object>;
if (singleEnumerable != null)
return singleEnumerable.Select(x => new[] {x});
throw new ArgumentException(string.Format("Property {0} on {1} did not return IEnumerable<object[]>", _propertyName, type.FullName));
}
private object[] GetObjects(object[] parameterized, ParameterInfo[] parameters, IFixture fixture)
{
var result = new object[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
if (i < parameterized.Length)
result[i] = parameterized[i];
else
result[i] = CustomizeAndCreate(fixture, parameters[i]);
}
return result;
}
private object CustomizeAndCreate(IFixture fixture, ParameterInfo p)
{
var customizations = p.GetCustomAttributes(typeof (CustomizeAttribute), false)
.OfType<CustomizeAttribute>()
.Select(attr => attr.GetCustomization(p));
foreach (var c in customizations)
{
fixture.Customize(c);
}
var context = new SpecimenContext(fixture);
return context.Resolve(p);
}
public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
{
foreach (var values in GetAllParameterObjects(methodUnderTest))
{
yield return GetObjects(values, methodUnderTest.GetParameters(), _createFixture());
}
}
}
What actually happens
The NumericSequence
[PropertyData]
defines two iterations.
The composition of NumericSequence
[PropertyData]
with [AutoData]
assumes that there is enough data on each iteration.
However, the actual composition is:
1st iteration: [PropertyData], [AutoData]
2nd iteration: [PropertyData], [n/a]
That's why in the 2nd iteration you eventually run out of data.
Composition
The CompositeDataAttribute
respects the LSP in a sense that it is programmed against the base of all data theories, the DataAttribute
class.
(That is, there is no assumption that all attributes are composed with [AutoData]
at the end.)
For that reason, it can't simply jump from the 2nd iteration to the 1st iteration and grab some [AutoData]
values – that would break the LSP.
What you could do
Make the actual composition look like:
1st iteration: [PropertyData], [AutoData]
2nd iteration: [PropertyData], [AutoData]
By defining two properties:
public static IEnumerable<object[]> FirstPropertyData { get {
yield return new object[] { 1 }; } }
public static IEnumerable<object[]> OtherPropertyData { get {
yield return new object[] { 9 }; } }
And then, the original test can be written as:
[Theory]
[AutoPropertyData("FirstPropertyData")]
[AutoPropertyData("OtherPropertyData")]
public void Test(int n1, int n2, int n3)
{
}
The test executes twice and n1
is always supplied by [PropertyData]
while n2
and n3
are always supplied by [AutoData]
.
As a workaround you can restructure the AutoPropertyDataAttribute
a bit and use the CompositeDataAttribute
internally, rather than deriving from it. Derive from the PropertyDataAttribute
instead:
public class AutoPropertyDataAttribute : PropertyDataAttribute
{
public AutoPropertyDataAttribute(string propertyName)
: base(propertyName)
{
}
Then override the GetData
method to loop over the values returned by the PropertyDataAttribute
, and leverage AutoFixture's InlineAutoData
(which derives from CompositeDataAttribute
) to fill in the rest of the parameters:
public override IEnumerable<object[]> GetData(System.Reflection.MethodInfo methodUnderTest, Type[] parameterTypes)
{
foreach (var values in base.GetData(methodUnderTest, parameterTypes))
{
// The params returned by the base class are the first m params,
// and the rest of the params can be satisfied by AutoFixture using
// its InlineAutoDataAttribute class.
var iada = new InlineAutoDataAttribute(values);
foreach (var parameters in iada.GetData(methodUnderTest, parameterTypes))
yield return parameters;
}
}
The outer loop iterates over the values returned by the PropertyData
(each iteration is a row, with some of the cells filled in). The inner loop fills in the remaining cells.
It's not the prettiest thing, but it seems to work. I like Mark's idea to have AutoFixture try to fill in the remaining cells. One less piece of glue code to write :)
Hope this helps,
Jeff.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With