使用AutoFixture为递归数据结构创建夹具
我正在开发一个项目,我有一些递归数据结构,我想为它创建一个夹具。
数据结构是XmlCommandElement
,它有一个方法ToCommand
将XmlCommandElement
转换为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(); 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(); 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(c => c.FromFactory(() => _xmlCommandElement) .OmitAutoProperties()); } private static string CreateRandomString() { return _fixture.Create<Generator>().First(); } private XmlCommandPropertyElement[] CreateProperties() { var properties = new List(); 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();
这完全没问题! 但我遇到的问题是它不是通用的 ,至少据我所知,AutoFixture的重点在于避免制作特定的灯具。
所以我真正想要做的就是这样(在这里找到它,但它对我不起作用。):
var fixture = new Fixture(); fixture.Behaviors.OfType() .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();
以下是所有参考代码:
接口:
public interface ICommandCollection : IEnumerable { ICommand this[string commandName] { get; } void Add(ICommand command); } public interface ICommandPropertyCollection : IEnumerable { 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 _properties; public CommandPropertyCollection() { _properties = new ConcurrentDictionary(); } 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 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(); }); $"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)); } } }
感谢Mark Seemann! 最终解决方案如下所示:
public class RecursiveCustomization : ICustomization { public int MaxDepth { get; set; } public int MaxElements { get; set; } public void Customize(IFixture fixture) { fixture.Behaviors .OfType() .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();
您可以通过更改Fixture的递归行为轻松地创建一个小树:
[Fact] public void CreateSmallTree() { var fixture = new Fixture(); fixture.Behaviors .OfType() .ToList() .ForEach(b => fixture.Behaviors.Remove(b)); fixture.Behaviors.Add(new OmitOnRecursionBehavior(recursionDepth: 2)); var xce = fixture.Create(); Assert.NotEmpty(xce.Commands); }
上述测试通过。