【问题标题】:Autofixture generate collection without duplicates (by ID)Autofixture 生成没有重复的集合(按 ID)
【发布时间】:2016-02-22 11:11:03
【问题描述】:

我有以下课程:

public class Foo
{
    public List<DescriptionInfo> Descriptions { get; set; }
}


public class DescriptionInfo
{
    public int LanguageId { get; set; }
    public string Value { get; set; }
}

我想使用 Autofixture 创建 Foo 实例。但是,LanguageId 必须来自预定义的列表。因此,我创建了以下自定义:

public class LanguageIdSpecimenBuilder : ISpecimenBuilder
{
    private static readonly List<int> LanguageIds = new List<int> {
        1,
        2,
        666,
    };

    public object Create(object request, ISpecimenContext context)
    {
        var info = request as PropertyInfo;
        if (info != null)
        {
            if (info.Name == "LanguageID")
            {
                return LanguageIds.GetRandomElement();
            }
        }
        return new NoSpecimen(request);
    }
}

现在一切都很好:

Fixture fixture = new Fixture();
fixture.Customizations.Add(new LanguageIdSpecimenBuilder());
var foo = fixture.Create<Foo>(); 

但是,还有一个要求:一个语言 ID 不能有重复的条目。我怎样才能做到这一点?

编辑: 例如,有效的实例是:

Foo1:
 - LanguageId: 1, Value: "english description_ae154c"
 - LanguageId: 2, Value: "zuzulu_510b7g"

Foo2:
 - LanguageId: 1, Value: "english description_87f5de"
 - LanguageId: 666, Value: "chinese_35e450"
 - LanguageId: 2, Value: "zuzulu_fe830d"

无效实例:

Foo1:
 - LanguageId: 1, Value: "_04dcd6"
 - LanguageId: 1, Value: "_66ccc4"
 - LanguageId: 2, Value: "zuzulu_c05b0f"

【问题讨论】:

  • 有效语言 ID 列表包含三个值。如果创建四个 Foo 实例会发生什么?
  • 我添加了示例。该要求适用于特定的 Foo 实例,而不是全局范围的。

标签: c# .net testing autofixture


【解决方案1】:

首先 - 让我建议您的 POC 可以更精确。如果您不允许列表中具有相同 LanguageID 的描述,您可以像这样改造您的 POC:

public class Foo
{
    public ISet<DescriptionInfo> Descriptions { get; set; }
}

public class DescriptionInfo
{
    public int LanguageID { get; set; }
    public string Value { get; set; }

    public override bool Equals(object obj)
    {
        var anotherInfo = (DescriptionInfo)obj;
        return anotherInfo.LanguageID == LanguageID;
    }

    public override int GetHashCode()
    {
        return LanguageID;
    }
}

问题就解决了:

fixture.Customizations.Add(new TypeRelay(typeof(ISet<DescriptionInfo>),
  typeof(HashSet<DescriptionInfo>)));
fixture.Customizations.Add(new LanguageIdSpecimenBuilder());
var foo = fixture.Create<Foo>();

如果您不能/不想改造您的 POC,您应该在自定义构建器中关注您想要正确设置的属性 - Descriptions

public object Create(object request, ISpecimenContext context)
{
    var info = request as PropertyInfo;
    if (info != null && info.Name == "Descriptions" && info.DeclaringType == typeof(Foo))
    {
        if (info.Name == "Descriptions")
        {
            return context.Create<List<DescriptionInfo>>()
             .GroupBy(g => g.LanguageId)
             .Select(g => g.First())
             .ToList();
         }
    }
    if (info != null && info.Name == "LanguageId" && info.DeclaringType == typeof(DescriptionInfo))
    {
        return LanguageIds.GetRandomElement();
    }
    return new NoSpecimen(request);
}

最后一点 - 你的代码 sn-p 是错误的 - 字符串比较区分大小写;所以它应该是info.Name == "LanguageId",检查声明属性的类型也是个好主意 - 而不仅仅是属性名称。

【讨论】:

  • 感谢您的详细解答。确实有一个案例错误&检查声明类型是一个非常好的主意。
【解决方案2】:

由于您希望 ID 值在单个 Foo 实例上是唯一的,因此您最好自定义 Foo 类型本身。

您可能希望让 AutoFixture 处理 Foo 的其他部分,DescriptionInfo 值除外。一种方法是创建一个ISpecimenCommand,它可以用作生成值的后处理器:

public class UniqueIDsOnFooCommand : ISpecimenCommand
{
    private static readonly int[] languageIds = new [] { 1, 2, 666, };
    private static readonly Random random = new Random();

    public void Execute(object specimen, ISpecimenContext context)
    {
        var foo = specimen as Foo;
        if (foo == null)
            return;

        foreach (var t in
            foo.Descriptions.Zip(Shuffle(languageIds), Tuple.Create))
        {
            var description = t.Item1;
            var id = t.Item2;
            description.LanguageId = id;
        }            
    }

    private static IEnumerable<T> Shuffle<T>(IReadOnlyCollection<T> source)
    {
        return source.OrderBy(_ => random.Next());
    }
}

此实现使用一个现成的已知语言 ID 数组 shuffles them,然后是带有描述的 zips them

您可以将此命令打包到更改 Foo 类型行为的自定义中:

public class FooCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customizations.Add(
            SpecimenBuilderNodeFactory.CreateTypedNode(
                typeof(Foo),
                new Postprocessor(
                    new MethodInvoker(
                        new ModestConstructorQuery()),
                    new CompositeSpecimenCommand(
                        new AutoPropertiesCommand(),
                        new UniqueIDsOnFooCommand()))));
    }
}

以下测试通过:

[Fact]
public void CreateFoos()
{
    var fixture = new Fixture().Customize(new FooCustomization());
    fixture.Behaviors.Add(new TracingBehavior());

    var foos = fixture.CreateMany<Foo>();

    Assert.True(
        foos.All(f => f
            .Descriptions
            .Select(x => x.LanguageId)
            .Distinct()
            .Count() == f.Descriptions.Count),
        "Languaged IDs should be unique for each foo.");
}

创建的Foo 值如下所示:

  Foo:
  - LanguageId: 1, Value: "Valueefd1268c-56e9-491a-a43d-3f385ea0dfd8"
  - LanguageId: 666, Value: "Value461d7130-7bc0-4e71-96a8-432c019567c9"
  - LanguageId: 2, Value: "Valuee2604a80-f113-485c-8276-f19a97bca505"

  Foo:
  - LanguageId: 2, Value: "Value66eee598-1895-4c0d-b3c6-4262711fe394"
  - LanguageId: 666, Value: "Valuef9a58482-13c6-4491-a690-27b243af6404"
  - LanguageId: 1, Value: "Valueeb133071-dad7-4bff-b8ef-61d3505f1641"

  Foo:
  - LanguageId: 1, Value: "Valuea1161dc6-75e5-45a3-9333-f6bca01e882b"
  - LanguageId: 666, Value: "Value31332272-5b61-4d51-8572-172b781e5f6b"
  - LanguageId: 2, Value: "Value83f80569-277d-49b2-86e5-be256075835e"

说了这么多,consider a different design that doesn't involve settable collection properties

【讨论】:

  • 也是一个不错的解决方案,即使在我看来,这比@ondrej-svejdar 的答案更“hacky”。但是因为他是第一个,所以我会保留他的回答。
  • @piotrwest 不用担心。重要的是,您的问题得到了有用的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-09-20
  • 2023-03-09
  • 2017-11-18
  • 1970-01-01
  • 2018-10-27
  • 2011-02-17
相关资源
最近更新 更多