Автофиксирование CompositeDataAttribute не работает с PropertyDataAttribute
Я пытаюсь создать AutoPropertyDataAttribute
основанный на CompositeDataAttribute
из этого примера AutoFixture: PropertyData и гетерогенные параметры.
Он работает с одним набором параметров, но не работает с несколькими наборами параметров. Вот код:
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()
})
{
}
}
Попытка раскомментировать второй yield
сломает тест с сообщением:
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)
То же самое происходит с ClassDataAttribute
3 ответа
Что на самом деле происходит
NumericSequence
[PropertyData]
определяет две итерации.
Состав NumericSequence
[PropertyData]
с [AutoData]
предполагает, что на каждой итерации достаточно данных.
Тем не менее, фактический состав:
1st iteration: [PropertyData], [AutoData]
2nd iteration: [PropertyData], [n/a]
Вот почему во второй итерации у вас в конце концов заканчиваются данные.
Состав
CompositeDataAttribute
уважает LSP в том смысле, что он запрограммирован на основе всех теорий данных, DataAttribute
учебный класс.
(То есть не предполагается, что все атрибуты состоят из [AutoData]
в конце.)
По этой причине он не может просто перейти от 2-й итерации к 1-й итерации и получить некоторые [AutoData]
ценности - это сломало бы LSP.
Что ты мог сделать
Сделать фактическую композицию похожей на:
1st iteration: [PropertyData], [AutoData]
2nd iteration: [PropertyData], [AutoData]
Определив два свойства:
public static IEnumerable<object[]> FirstPropertyData { get {
yield return new object[] { 1 }; } }
public static IEnumerable<object[]> OtherPropertyData { get {
yield return new object[] { 9 }; } }
И тогда оригинальный тест можно записать так:
[Theory]
[AutoPropertyData("FirstPropertyData")]
[AutoPropertyData("OtherPropertyData")]
public void Test(int n1, int n2, int n3)
{
}
Тест выполняется дважды и n1
всегда поставляется [PropertyData]
в то время как n2
а также n3
всегда поставляются [AutoData]
,
Я столкнулся с этой проблемой и решил реализовать собственный DataAttribute для решения проблемы. Я не мог использовать ни один атрибут в качестве базового класса (причины приведены ниже), поэтому я просто взял то, что мне нужно, из источника каждого. Спасибо OSS:)
Что следует отметить:
- Я хотел немного изменить семантику, чтобы у меня была возможность выдавать отдельные объекты, а не массивы. Просто делает код более аккуратным для параметров одного объекта. Это означало, что я не мог использовать
PropertyDataAttribute
как базовый класс - Приспособление должно быть создано каждый раз, когда генерируется новый набор параметров. Это означало, что я не мог использовать
AutoDataAttribute
как базовый класс
Или встроенный источник ниже:
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());
}
}
}
В качестве обходного пути вы можете реструктурировать AutoPropertyDataAttribute
немного и использовать CompositeDataAttribute
внутренне, а не исходя из этого. Производная от PropertyDataAttribute
вместо:
public class AutoPropertyDataAttribute : PropertyDataAttribute
{
public AutoPropertyDataAttribute(string propertyName)
: base(propertyName)
{
}
Затем переопределите GetData
метод зацикливания значений, возвращаемых PropertyDataAttribute
и использовать AutoFixture's InlineAutoData
(который вытекает из CompositeDataAttribute
) заполнить остальные параметры:
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;
}
}
Внешний цикл перебирает значения, возвращаемые PropertyData
(каждая итерация представляет собой строку с заполненными некоторыми ячейками). Внутренняя петля заполняет остальные ячейки.
Это не самая красивая вещь, но, кажется, работает. Мне нравится идея Марка, чтобы AutoFixture пытался заполнить оставшиеся ячейки. Еще один кусок клея для написания кода:)
Надеюсь это поможет,
Джефф.