集合的 SpecimenBuilder
SpecimenBuilder for a collection
我需要自定义创建一个集合,其中的对象之间的关系非常复杂,我不知道如何正确地做。
为了这个问题,让我们假设我正在开发一个待办事项应用程序。它有 Item
s 和 SubItem
s,并且项目有一个周数,指示何时应该完成:
public class Item {
public string Name { get; set; }
public int Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
}
public class SubItem {
public string Name { get; set; }
public Item Parent { get; set; }
}
现在,因为这是实际应用程序中数据通常的样子,我想创建一个具有以下属性的项目集合:
- 有些项目名称相同,但周数不同
- 有些项目的周数相同但名称不同
- 有同名但父项不同的子项
为了做到这一点,我创建了一个 TodoItemSpecimenBuilder : ISpecimenBuilder
来启动它的 Create 方法,如下所示:
var type = (request as PropertyInfo)?.PropertyType ?? request as Type;
if (type == null || !typeof(IEnumerable<Item>).IsAssignableFrom(type))
{
return new NoSpecimen();
}
// build up the actual collection
return BuildActualCollection();
然而,当我 运行 使用我的上下文中包含的这个样本构建器进行测试时,我什至在输入我的设置代码之前就对 return 语句进行了很多(可能 20 或 30)次点击,我第一次尝试 CreateMany<Item>()
时,它会抛出异常,因为它无法将 OmitSpecimen
转换为 Item
。
我做错了什么?
完整示例代码,可在安装 NUnit 和 AutoFixture 后编译:
public class TodoList
{
public ICollection<Item> Tasks { get; set; }
}
public class Item
{
public string Name { get; set; }
public Week Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
public int ItemId { get; set; }
public TodoList TodoList { get; set; }
}
public class SubItem
{
public Item Item { get; set; }
public string Name { get; set; }
public int SortOrder { get; set; }
public string HelpText { get; set; }
}
public class Week
{
public int WeekId { get; set; }
}
public class ItemCollectionSpecimenBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (!IsApplicable(request))
{
return new NoSpecimen();
}
var items = new List<Item>(3);
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week2));
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
ConfigureSubItems(context, items);
return items;
}
private static bool IsApplicable(object request)
{
bool IsManyItemsType(Type type) => typeof(IEnumerable<Item>).IsAssignableFrom(type);
bool IsItemsType(Type type) => type != null && typeof(Item) == type;
switch (request)
{
case PropertyInfo pInfo:
return IsManyItemsType(pInfo.PropertyType);
case Type type:
return IsManyItemsType(type);
case MultipleRequest multipleRequest:
if (!(multipleRequest.Request is SeededRequest seededRequest))
{
return false;
}
return IsItemsType(seededRequest.Request as Type);
default:
return false;
}
}
private static Item CreateItem(ISpecimenContext context, Week week)
{
var item = context.Create<Item>();
item.Week = week;
return item;
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
{
name = item.Name;
}
else
{
item.Name = name;
}
}
}
private static void ConfigureSubItems(ISpecimenContext context, IEnumerable<Item> items)
{
foreach (var group in items.GroupBy(item => item.Week.WeekId))
{
var subItemTemplates = context.CreateMany<SubItem>().ToList();
foreach (var item in group)
{
item.SubItems.Clear();
foreach (var subItem in context.CreateMany<SubItem>().Zip(subItemTemplates,
(model, subItem) =>
{
subItem.Item = item;
subItem.Name = model.Name;
subItem.SortOrder = model.SortOrder;
subItem.HelpText = model.HelpText;
return subItem;
}))
{
item.SubItems.Add(subItem);
}
}
}
}
}
[TestFixture]
public class AutoFixtureSpecimenBuilderTests
{
private static void TestCreationOfTasks(Func<IFixture, ICollection<Item>> creator)
{
var fixture = new Fixture();
fixture.Customizations.Add(new ItemCollectionSpecimenBuilder());
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
var tasks = creator(fixture);
Assert.AreEqual(3, tasks.Count);
Assert.AreEqual(2, tasks.GroupBy(t => t.Week).Count());
Assert.IsTrue(tasks.GroupBy(t => t.Week).Select(g => g.Select(t => t.Name).Distinct()).All(distinctNames => distinctNames.Count() == 1));
var task = tasks.GroupBy(t => t.Week).OrderBy(g => g.Count()).First().OrderBy(t => t.ItemId).First();
}
[Test]
public void CreateMany() => TestCreationOfTasks(fixture => fixture.CreateMany<Item>().ToList());
[Test]
public void CreateWithProperty() => TestCreationOfTasks(fixture => fixture.Create<TodoList>().Tasks);
[Test]
public void CreateAsList() => TestCreationOfTasks(fixture => fixture.Create<IList<Item>>());
}
我想不出有什么特别好的方法来解决这个问题。问题是 Item
是一个递归(树状)数据结构,而 却不容易扩展。
当您创建一个 ISpecimenBuilder
时,您告诉 AutoFixture 该对象将处理对特定对象的请求。这意味着您不能再使用 context
来请求这些对象,因为这将递归回同一个构建器,从而导致无限递归。
因此,一种选择是在构建器中构建对象 'by hand'。您仍然可以请求所有其他类型,但您必须避免请求导致递归的对象。
另一种选择是。这是一个概念证明:
public class ItemCollectionSpecimenCommand : ISpecimenCommand
{
public void Execute(object specimen, ISpecimenContext context)
{
var @is = specimen as IEnumerable<Item>;
if (@is == null)
return;
var items = @is.ToList();
if (items.Count < 3)
return;
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items[0].Week = week1;
items[1].Week = week1;
items[2].Week = week2;
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
name = item.Name;
else
item.Name = name;
}
}
}
您可以像这样配置您的灯具:
var fixture = new Fixture();
fixture.Customizations.Add(
SpecimenBuilderNodeFactory.CreateTypedNode(
typeof(IEnumerable<Item>),
new Postprocessor(
new EnumerableRelay(),
new CompositeSpecimenCommand(
new AutoPropertiesCommand(),
new ItemCollectionSpecimenCommand()))));
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
这将通过重现测试 CreateWithProperty
和 CreateAsList
,但不会通过 CreateMany
。
由于各种(历史)原因,CreateMany
的工作方式与 Create<IList<>>
之类的工作方式完全不同。如果你真的需要它也能为 CreateMany
工作,我会看看我能做些什么,但我不能保证这会成为可能。
看了几个小时后,这是我能想到的最好的。我已经一两年没有真正使用过 AutoFixture,所以我可能只是身材走样了,并且有更好的解决方案 是 可用......我只是可以'没想到...
我需要自定义创建一个集合,其中的对象之间的关系非常复杂,我不知道如何正确地做。
为了这个问题,让我们假设我正在开发一个待办事项应用程序。它有 Item
s 和 SubItem
s,并且项目有一个周数,指示何时应该完成:
public class Item {
public string Name { get; set; }
public int Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
}
public class SubItem {
public string Name { get; set; }
public Item Parent { get; set; }
}
现在,因为这是实际应用程序中数据通常的样子,我想创建一个具有以下属性的项目集合:
- 有些项目名称相同,但周数不同
- 有些项目的周数相同但名称不同
- 有同名但父项不同的子项
为了做到这一点,我创建了一个 TodoItemSpecimenBuilder : ISpecimenBuilder
来启动它的 Create 方法,如下所示:
var type = (request as PropertyInfo)?.PropertyType ?? request as Type;
if (type == null || !typeof(IEnumerable<Item>).IsAssignableFrom(type))
{
return new NoSpecimen();
}
// build up the actual collection
return BuildActualCollection();
然而,当我 运行 使用我的上下文中包含的这个样本构建器进行测试时,我什至在输入我的设置代码之前就对 return 语句进行了很多(可能 20 或 30)次点击,我第一次尝试 CreateMany<Item>()
时,它会抛出异常,因为它无法将 OmitSpecimen
转换为 Item
。
我做错了什么?
完整示例代码,可在安装 NUnit 和 AutoFixture 后编译:
public class TodoList
{
public ICollection<Item> Tasks { get; set; }
}
public class Item
{
public string Name { get; set; }
public Week Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
public int ItemId { get; set; }
public TodoList TodoList { get; set; }
}
public class SubItem
{
public Item Item { get; set; }
public string Name { get; set; }
public int SortOrder { get; set; }
public string HelpText { get; set; }
}
public class Week
{
public int WeekId { get; set; }
}
public class ItemCollectionSpecimenBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (!IsApplicable(request))
{
return new NoSpecimen();
}
var items = new List<Item>(3);
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week2));
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
ConfigureSubItems(context, items);
return items;
}
private static bool IsApplicable(object request)
{
bool IsManyItemsType(Type type) => typeof(IEnumerable<Item>).IsAssignableFrom(type);
bool IsItemsType(Type type) => type != null && typeof(Item) == type;
switch (request)
{
case PropertyInfo pInfo:
return IsManyItemsType(pInfo.PropertyType);
case Type type:
return IsManyItemsType(type);
case MultipleRequest multipleRequest:
if (!(multipleRequest.Request is SeededRequest seededRequest))
{
return false;
}
return IsItemsType(seededRequest.Request as Type);
default:
return false;
}
}
private static Item CreateItem(ISpecimenContext context, Week week)
{
var item = context.Create<Item>();
item.Week = week;
return item;
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
{
name = item.Name;
}
else
{
item.Name = name;
}
}
}
private static void ConfigureSubItems(ISpecimenContext context, IEnumerable<Item> items)
{
foreach (var group in items.GroupBy(item => item.Week.WeekId))
{
var subItemTemplates = context.CreateMany<SubItem>().ToList();
foreach (var item in group)
{
item.SubItems.Clear();
foreach (var subItem in context.CreateMany<SubItem>().Zip(subItemTemplates,
(model, subItem) =>
{
subItem.Item = item;
subItem.Name = model.Name;
subItem.SortOrder = model.SortOrder;
subItem.HelpText = model.HelpText;
return subItem;
}))
{
item.SubItems.Add(subItem);
}
}
}
}
}
[TestFixture]
public class AutoFixtureSpecimenBuilderTests
{
private static void TestCreationOfTasks(Func<IFixture, ICollection<Item>> creator)
{
var fixture = new Fixture();
fixture.Customizations.Add(new ItemCollectionSpecimenBuilder());
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
var tasks = creator(fixture);
Assert.AreEqual(3, tasks.Count);
Assert.AreEqual(2, tasks.GroupBy(t => t.Week).Count());
Assert.IsTrue(tasks.GroupBy(t => t.Week).Select(g => g.Select(t => t.Name).Distinct()).All(distinctNames => distinctNames.Count() == 1));
var task = tasks.GroupBy(t => t.Week).OrderBy(g => g.Count()).First().OrderBy(t => t.ItemId).First();
}
[Test]
public void CreateMany() => TestCreationOfTasks(fixture => fixture.CreateMany<Item>().ToList());
[Test]
public void CreateWithProperty() => TestCreationOfTasks(fixture => fixture.Create<TodoList>().Tasks);
[Test]
public void CreateAsList() => TestCreationOfTasks(fixture => fixture.Create<IList<Item>>());
}
我想不出有什么特别好的方法来解决这个问题。问题是 Item
是一个递归(树状)数据结构,而
当您创建一个 ISpecimenBuilder
时,您告诉 AutoFixture 该对象将处理对特定对象的请求。这意味着您不能再使用 context
来请求这些对象,因为这将递归回同一个构建器,从而导致无限递归。
因此,一种选择是在构建器中构建对象 'by hand'。您仍然可以请求所有其他类型,但您必须避免请求导致递归的对象。
另一种选择是
public class ItemCollectionSpecimenCommand : ISpecimenCommand
{
public void Execute(object specimen, ISpecimenContext context)
{
var @is = specimen as IEnumerable<Item>;
if (@is == null)
return;
var items = @is.ToList();
if (items.Count < 3)
return;
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items[0].Week = week1;
items[1].Week = week1;
items[2].Week = week2;
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
name = item.Name;
else
item.Name = name;
}
}
}
您可以像这样配置您的灯具:
var fixture = new Fixture();
fixture.Customizations.Add(
SpecimenBuilderNodeFactory.CreateTypedNode(
typeof(IEnumerable<Item>),
new Postprocessor(
new EnumerableRelay(),
new CompositeSpecimenCommand(
new AutoPropertiesCommand(),
new ItemCollectionSpecimenCommand()))));
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
这将通过重现测试 CreateWithProperty
和 CreateAsList
,但不会通过 CreateMany
。
由于各种(历史)原因,CreateMany
的工作方式与 Create<IList<>>
之类的工作方式完全不同。如果你真的需要它也能为 CreateMany
工作,我会看看我能做些什么,但我不能保证这会成为可能。
看了几个小时后,这是我能想到的最好的。我已经一两年没有真正使用过 AutoFixture,所以我可能只是身材走样了,并且有更好的解决方案 是 可用......我只是可以'没想到...