AutoFixture CompositeDataAttribute does not work with PropertyDataAttribute

I am trying to create an AutoPropertyDataAttribute based on the CompositeDataAttribute from this AutoFixture: PropertyData example and dissimilar parameters .

It works with one set of parameters, but not with a large set of parameters. Here is the 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 the test with the 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) 

The same thing happens with ClassDataAttribute

+6
source share
3 answers

What is really going on

NumericSequence [PropertyData] defines two iterations.

NumericSequence [PropertyData] with [AutoData] assumes there is enough data at each iteration.

However, the actual composition:

 1st iteration: [PropertyData], [AutoData] 2nd iteration: [PropertyData], [n/a] 

This is why in the 2nd iteration you end up running out of data.

Structure

CompositeDataAttribute refers to LSP in the sense that it is programmed against the base of all data theories, the DataAttribute class.

(That is, there is no assumption that all attributes consist of [AutoData] at the end.)

For this reason, it cannot just go from the 2nd iteration to the 1st iteration and capture some [AutoData] values, which will break the LSP.

What could you 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 is run twice, and n1 always provided [PropertyData] , and n2 and n3 always provided [AutoData] .

+2
source

I ran into this problem and decided to implement a custom DataAttribute to solve the problem. I could not use any attribute as a base class (reasons below), so I just took what I needed from each source. Thanks, OSS :)

Notes:

  • I wanted to change the semantics a bit, so that I could get individual objects, not arrays. It just makes the code more convenient for the parameters of a single object. This meant that I could not use PropertyDataAttribute as a base class.
  • The device must be created every time a new set of parameters is generated. This meant that I could not use AutoDataAttribute as a base class.

Gist

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()); } } } 
+3
source

As a workaround, you can restructure AutoPropertyDataAttribute and use CompositeDataAttribute internally instead of extracting from it. Output instead of PropertyDataAttribute :

 public class AutoPropertyDataAttribute : PropertyDataAttribute { public AutoPropertyDataAttribute(string propertyName) : base(propertyName) { } 

Then override the GetData method to loop over the values ​​returned by PropertyDataAttribute and use AutoFixture InlineAutoData (which comes 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 repeats over the values ​​returned by the PropertyData (each iteration is a string, with some of the cells filled). An inner loop fills the remaining cells.

This is not the most beautiful thing, but it seems to work. I like Mark's idea for AutoFixture to try to fill in the remaining cells. Another piece of code to write:

Hope this helps,
Jeff.

+1
source

Source: https://habr.com/ru/post/953991/


All Articles