SpecimenBuilder for a collection

浪子不回头ぞ 提交于 2019-12-11 04:25:23

问题


I have a need for customizing creation of a collection, with quite complicated relationships between the objects within it, and I can't figure out how to do it correctly.

For the sake of this issue, let's assume I'm working on a todo app. It has Items and SubItems, and the items have a week number indicating when they should be done:

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; }
}

Now, because this is what data usually looks like in the actual application, I want to create a collection of Items that has the following properties:

  • There are items that have the same name, but different weeks
  • There are items that have the same week but different name
  • There are sub-items that have the same name, but different parents

In order to do this, I've created a TodoItemSpecimenBuilder : ISpecimenBuilder which starts its Create method like this:

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();

However, when I run tests with this specimen builder included in my context, I get lots (maybe 20 or 30) hits on the return statement before I enter even my setup code, and the first time I try to actually CreateMany<Item>(), it blows up with a cast exception because it can't cast OmitSpecimen to Item.

What am I doing wrong here?


Full sample code, compilable after installing NUnit and 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>>());
}

回答1:


I can't think of any particularly good way to address this issue. The problem is that Item is a recursive (tree-like) data structure, and while AutoFixture does have some support for such, it's not easily extensible.

When you create an ISpecimenBuilder, you tell AutoFixture that this object is going to handle requests for particular objects. This means that you can no longer use the context to request those objects, because that'll recurse back into the same builder, causing an infinite recursion.

So, one option is to build up the objects 'by hand' from within the builder. You can still request all other types, but you'll have to avoid requesting objects that cause recursion.

Another option is to add a post-processor. Here's a proof of concept:

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;
        }
    }
}

You can configure your fixture like this:

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());

This'll pass the repro tests CreateWithProperty and CreateAsList, but not CreateMany.

For various (historical) reasons, the way that CreateMany works is quite different from the way that something like Create<IList<>> works. If you really need this to work for CreateMany as well, I'll see what I can do, but I can't promise that this'll be possible at all.

After having looked at this repro for a few hours, this is the best I can come up with. I haven't really used AutoFixture for a year or two now, so it's possible that I'm simply out of shape, and that a better solution is available... I just can't think of it...



来源:https://stackoverflow.com/questions/47512841/specimenbuilder-for-a-collection

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!