【问题标题】:Create a fixture for recursive data structure with AutoFixture使用 AutoFixture 为递归数据结构创建夹具
【发布时间】:2016-06-02 07:08:31
【问题描述】:

我正在做一个项目,我有一些递归数据结构,我想为它创建一个夹具。

数据结构是XmlCommandElement,它有一个方法ToCommandXmlCommandElement转换为Command

树上的每个节点都可以是XmlCommandElement 和/或XmlCommandPropertyElement

现在,为了测试 ToCommand 方法的行为,我想获取带有一些任意数据的 XmlCommandElement

我想控制树的深度以及每个节点的XmlCommandElement 和/或XmlCommandPropertyElement 实例的数量。

这是我用于夹具的代码:

public class XmlCommandElementFixture : ICustomization
{
    private static readonly Fixture _fixture = new Fixture();

    private XmlCommandElement _xmlCommandElement;

    public int MaxCommandsPerDepth { get; set; }

    public int MaxDepth { get; set; }

    public int MaxPropertiesPerCommand { get; set; }

    public XmlCommandElementFixture BuildCommandTree()
    {
        _xmlCommandElement = new XmlCommandElement();

        var tree = new Stack<XmlCommandElementNode>();

        tree.Push(new XmlCommandElementNode(0, _xmlCommandElement));

        while (tree.Count > 0) {
            var node = tree.Pop();
            node.Command.Key = CreateRandomString();
            node.Command.Properties = CreateProperties();

            if (MaxDepth > node.Depth) {
                var commands = new List<XmlCommandElement>();

                for (var i = 0; i < MaxCommandsPerDepth; i++) {
                    var command = new XmlCommandElement();
                    tree.Push(new XmlCommandElementNode(node.Depth + 1, command));
                    commands.Add(command);
                }

                node.Command.Commands = commands.ToArray();
            }
        }

        return this;
    }

    public void Customize(IFixture fixture)
    {
        fixture.Customize<XmlCommandElement>(c => c.FromFactory(() => _xmlCommandElement)
                                                   .OmitAutoProperties());
    }

    private static string CreateRandomString()
    {
        return _fixture.Create<Generator<string>>().First();
    }

    private XmlCommandPropertyElement[] CreateProperties()
    {
        var properties = new List<XmlCommandPropertyElement>();

        for (var i = 0; i < MaxPropertiesPerCommand; i++) {
            properties.Add(new XmlCommandPropertyElement {
                Key = CreateRandomString(),
                Value = CreateRandomString()
            });
        }

        return properties.ToArray();
    }

    private struct XmlCommandElementNode
    {
        public XmlCommandElementNode(int depth, XmlCommandElement xmlCommandElement)
        {
            Depth = depth;

            Command = xmlCommandElement;
        }

        public XmlCommandElement Command { get; }

        public int Depth { get; }
    }
}

这就是我的使用方式:

xmlCommandElement = new Fixture().Customize(new XmlCommandElementFixture {
    MaxDepth = 2,
    MaxCommandsPerDepth = 3,
    MaxPropertiesPerCommand = 4
}.BuildCommandTree()).Create<XmlCommandElement>();

这工作得很好!但我遇到的问题是它不是通用,至少据我所知,AutoFixture 的全部意义在于避免制作特定的固定装置。

所以我真正想做的是这样的(找到它here,但它对我不起作用。):

var fixture = new Fixture();
fixture.Behaviors.OfType<ThrowingRecursionBehavior>()
       .ToList()
       .ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new DepthThrowingRecursionBehavior(2));
fixture.Behaviors.Add(new OmitOnRecursionForRequestBehavior(typeof(XmlCommandElement), 3));
fixture.Behaviors.Add(new OmitOnRecursionForRequestBehavior(typeof(XmlCommandPropertyElement), 4));

xmlCommandElement = fixture.Create<XmlCommandElement>();

以下是所有代码供参考:

接口:

public interface ICommandCollection : IEnumerable<ICommand>
{
    ICommand this[string commandName] { get; }

    void Add(ICommand command);
}

public interface ICommandPropertyCollection : IEnumerable<ICommandProperty>
{
    string this[string key] { get; }

    void Add(ICommandProperty property);
}

public interface ICommandProperty
{
    string Key { get; }

    string Value { get; }
}

public interface ICommand
{
    ICommandCollection Children { get; set; }

    string Key { get; }

    ICommandPropertyCollection Properties { get; }
}

public interface ICommandConvertible
{
    ICommand ToCommand();
}

类:

public sealed class CommandPropertyCollection : ICommandPropertyCollection
{
    private readonly IDictionary<string, ICommandProperty> _properties;

    public CommandPropertyCollection()
    {
        _properties = new ConcurrentDictionary<string, ICommandProperty>();
    }

    public string this[string key]
    {
        get
        {
            ICommandProperty property = null;

            _properties.TryGetValue(key, out property);

            return property.Value;
        }
    }

    public void Add(ICommandProperty property)
    {
        _properties.Add(property.Key, property);
    }

    public IEnumerator<ICommandProperty> GetEnumerator()
    {
        return _properties.Values.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public sealed class CommandProperty : ICommandProperty
{
    public CommandProperty(string key, string value)
    {
        Key = key;

        Value = value;
    }

    public string Key { get; }

    public string Value { get; }
}

public sealed class Command : ICommand
{
    public Command(string key, ICommandPropertyCollection properties)
    {
        Key = key;

        Properties = properties;
    }

    public ICommandCollection Children { get; set; }

    public string Key { get; }

    public ICommandPropertyCollection Properties { get; }
}

public class XmlCommandPropertyElement : ICommandPropertyConvertible
{
    [XmlAttribute("key")]
    public string Key { get; set; }

    [XmlAttribute("value")]
    public string Value { get; set; }

    public ICommandProperty ToCommandProperty()
    {
        return new CommandProperty(Key, Value);
    }
}

最后,我要测试的类如下:

public class XmlCommandElement : ICommandConvertible
{
    [XmlArray]
    [XmlArrayItem("Command", typeof(XmlCommandElement))]
    public XmlCommandElement[] Commands { get; set; }

    [XmlAttribute("key")]
    public string Key { get; set; }

    [XmlArray]
    [XmlArrayItem("Property", typeof(XmlCommandPropertyElement))]
    public XmlCommandPropertyElement[] Properties { get; set; }

    public ICommand ToCommand()
    {
        ICommandPropertyCollection properties = new CommandPropertyCollection();

        foreach (var property in Properties) {
            properties.Add(property.ToCommandProperty());
        }

        ICommand command = new Command(Key, properties);

        return command;
    }
}

测试本身如下所示:

namespace Yalla.Tests.Commands
{
    using Fixtures;

    using FluentAssertions;

    using Ploeh.AutoFixture;

    using Xbehave;

    using Yalla.Commands;
    using Yalla.Commands.Xml;

    public class XmlCommandElementTests
    {
        [Scenario]
        public void ConvertToCommand(XmlCommandElement xmlCommandElement, ICommand command)
        {
            $"Given an {nameof(XmlCommandElement)}"
                .x(() =>
                {
                    xmlCommandElement = new Fixture().Customize(new XmlCommandElementFixture {
                        MaxDepth = 2,
                        MaxCommandsPerDepth = 3,
                        MaxPropertiesPerCommand = 4
                    }.BuildCommandTree()).Create<XmlCommandElement>();
                });

            $"When the object is converted into {nameof(ICommand)}"
                .x(() => command = xmlCommandElement.ToCommand());

            "Then we need to have a root object with a key"
                .x(() => command.Key.Should().NotBeNullOrEmpty());

            "And 4 properties as its children"
                .x(() => command.Properties.Should().HaveCount(4));
        }
    }
}

感谢马克西曼!最终解决方案如下所示:

public class RecursiveCustomization : ICustomization
{
    public int MaxDepth { get; set; }

    public int MaxElements { get; set; }

    public void Customize(IFixture fixture)
    {
        fixture.Behaviors
               .OfType<ThrowingRecursionBehavior>()
               .ToList()
               .ForEach(b => fixture.Behaviors.Remove(b));
        fixture.Behaviors.Add(new OmitOnRecursionBehavior(MaxDepth));
        fixture.RepeatCount = MaxElements;
    }
}

并且可以这样使用:

xmlCommandElement = new Fixture().Customize(new RecursiveCustomization {
    MaxDepth = 2,
    MaxElements = 3
}).Create<XmlCommandElement>();

【问题讨论】:

  • 你应该有小单元来测试,非常小的方法,非常小的类。如果您观察数据经历了整个过程,那么您正在执行可能更大的集成测试。但是,您可以快速测试您的测试以确保它们为您的测试提供正确的输出,但是,我认为它试图向您展示代码味道,并且您应该将您的方法重构为更小。跨度>
  • @CallumLinington 谢谢你的评论!但我不确定它是如何相关的,我有递归数据结构,我想用一些数据加载它以测试它,这个夹具类负责生成数据,它不是我的类实际测试!我实际测试的类是 XmlCommandElement。
  • 这能回答你的问题吗? *.com/q/17087788/126014
  • @MarkSeemann 我试过了,它对我不起作用,我得到“System.InvalidOperationException”。
  • XmlCommandPropertyElement 是什么? ICommand 是什么? CommandPropertyCollection 是什么? ICommandPropertyCollection?你到底想完成什么?

标签: c# .net unit-testing autofixture


【解决方案1】:

您可以通过更改 Fixture 的递归行为相当轻松地创建一棵小树:

[Fact]
public void CreateSmallTree()
{
    var fixture = new Fixture();
    fixture.Behaviors
        .OfType<ThrowingRecursionBehavior>()
        .ToList()
        .ForEach(b => fixture.Behaviors.Remove(b));
    fixture.Behaviors.Add(new OmitOnRecursionBehavior(recursionDepth: 2));

    var xce = fixture.Create<XmlCommandElement>();

    Assert.NotEmpty(xce.Commands);
}

以上测试通过。

【讨论】:

  • 太棒了!超出深度我可以控制生成实例的数量吗?比如每个节点有 5 个 XmlCommandElement 和 6 个 XmlCommandPropertyElement 实例?
  • @EyalSolnik 您可以使用RepeatCount 来普遍更改每当请求“许多”元素时生成的元素数量,但不会比这更精细。
  • 感激不尽!谢谢你!我只是不想在我的测试中依赖默认值,RepeatCount 完成了工作!