9

如何设置确定性测试来验证列表中的项目是否有序?

首先,我做了以下事情:

public void SyncListContainsSortedItems(
    [Frozen] SyncItem[] expected, 
    SyncItemList sut)
{
    Assert.Equal(expected.OrderBy(x => x.Key).First(), sut.First());
}

但与所有好的测试一样,我在修改代码之前首先寻找失败。当然,这成功了,幸运的是,后来又失败了。所以我最初的失败不是确定性的。

其次,我做了以下事情,认为“这肯定会导致失败”:

public void SyncListContainsSortedItems(
    [Frozen] SyncItem[] seed, 
    SyncItemList sut)
{
    var expected = seed.OrderByDescending(x => x.Key);
    Assert.Equal(expected.OrderBy(x => x.Key).First(), sut.First());
}

令我惊讶的是,它也没有提供确定性失败。我意识到这是因为冷冻种子可能是自然降序开始的,所以我真的没有改善这种情况。

现在,我的实现不会对通过构造函数的项目进行排序。如何为我的测试建立可靠的基线?

附加信息同步项目列表代码如下所示。它并不多,因为它是我正在探索的设计:

public class SyncItemList : List<SyncItem>
{
    public SyncItemList(SyncItem[] input)
    {
        foreach (var item in input) { this.Add(item); }
    }
}

更新我一直在开发测试。以下工作但冗长。

public void SyncListContainsSortedItems(IFixture fixture, List<SyncItem> seed)
{
    var seconditem = seed.OrderBy(x => x.Key).Skip(1).First();
    seed.Remove(seconditem);
    seed.Insert(0, seconditem);
    var seedArray = seed.ToArray();

    var ascending = seedArray.OrderBy(x => x.Key).ToArray();
    var descending = seedArray.OrderByDescending(x => x.Key).ToArray();
    Assert.NotEqual(ascending, seedArray);
    Assert.NotEqual(descending, seedArray);

    fixture.Inject<SyncItem[]>(seedArray);
    var sut = fixture.Create<SyncItemList>();

    var expected = ascending;
    var actual = sut.ToArray();
    Assert.Equal(expected, actual);
}

改变我的实现以使其通过的一种简单方法是继承自SortedSet<SyncItem>而不是List<SyncItem>.

4

1 回答 1

12

有多种方法可以解决这个问题。

命令式版本

这是一个比 OP 中提供的更简单的命令式版本:

[Fact]
public void ImperativeTest()
{
    var fixture = new Fixture();
    var expected = fixture.CreateMany<SyncItem>(3).OrderBy(si => si.Key);
    var unorderedItems = expected.Skip(1).Concat(expected.Take(1)).ToArray();
    fixture.Inject(unorderedItems);

    var sut = fixture.Create<SyncItemList>();

    Assert.Equal(expected, sut);
}

虽然许多项目的默认数量3,但我认为最好在这个测试用例中明确地调用它。这里使用的加扰算法利用了这样一个事实,即在对三个(不同的)元素的序列进行排序之后,将第一个元素移到后面必然会产生一个无序列表。

但是,这种方法的问题在于它依赖于对 . 进行变异fixture,因此很难重构为更具声明性的方法。

定制版

为了重构为更具声明性的版本,您可以首先将加扰算法封装在Customization中:

public class UnorderedSyncItems : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customizations.Add(new UnorderedSyncItemsGenerator());
    }

    private class UnorderedSyncItemsGenerator : ISpecimenBuilder
    {
        public object Create(object request, ISpecimenContext context)
        {
            var t = request as Type;
            if (t == null ||
                t != typeof(SyncItem[]))
                return new NoSpecimen(request);

            var items = ((IEnumerable)context
                .Resolve(new FiniteSequenceRequest(typeof(SyncItem), 3)))
                .Cast<SyncItem>();
            return items.Skip(1).Concat(items.Take(1)).ToArray();
        }
    }
}

解析 a只是创建有限实例new FiniteSequenceRequest(typeof(SyncItem), 3))序列的弱类型(非泛型)方法;SyncItem这就是CreateMany<SyncItem>(3)幕后的工作。

这使您能够将测试重构为:

[Fact]
public void ImperativeTestWithCustomization()
{
    var fixture = new Fixture().Customize(new UnorderedSyncItems());
    var expected = fixture.Freeze<SyncItem[]>().OrderBy(si => si.Key);

    var sut = fixture.Create<SyncItemList>();

    Assert.Equal(expected, sut);
}

注意冻结方法的使用。这是必要的,因为UnorderedSyncItems定制只会改变SyncItem[]创建实例的方式;每次收到请求时,它仍然会创建一个新数组。Freeze确保每次都重用相同的数组 - 在fixture创建sut实例时也是如此。

基于约定的测试

[UnorderedConventions]通过引入一个属性,上述测试可以重构为声明性的、基于约定的测试:

public class UnorderedConventionsAttribute : AutoDataAttribute
{
    public UnorderedConventionsAttribute()
        : base(new Fixture().Customize(new UnorderedSyncItems()))
    {
    }
}

这只是应用UnorderedSyncItems定制的声明粘合剂。现在测试变成:

[Theory, UnorderedConventions]
public void ConventionBasedTest(
    [Frozen]SyncItem[] unorderedItems,
    SyncItemList sut)
{
    var expected = unorderedItems.OrderBy(si => si.Key);
    Assert.Equal(expected, sut);
}

注意[UnorderedSyncItems]and[Frozen]属性的使用。

这个测试非常简洁,但可能不是你想要的。问题是行为的变化现在隐藏在[UnorderedSyncItems]属性中,所以它相当隐含正在发生的事情。我更喜欢对整个测试套件使用相同的定制作为一组约定,所以我不喜欢在这个级别引入测试用例变体。但是,如果您的约定规定SyncItem[]实例应该始终是无序的,那么这个约定是好的。

但是,如果您只想对某些测试用例使用无序数组,则使用[AutoData]属性并不是最佳方法。

声明式测试用例

如果您可以简单地应用参数级属性,就像[Frozen]属性一样 - 也许将它们组合起来,例如[Unordered][Frozen]. 但是,这种方法行不通。

请注意前面的示例,顺序很重要。您必须UnorderedSyncItems在 Freezing 之前申请,否则可能无法保证被冻结的数组是无序的。

参数级属性的问题[Unordered][Frozen]在于,当它编译时,当 AutoFixture xUnit.net 粘合库读取和应用属性时,.NET 框架不保证属性的顺序。

相反,您可以定义一个要应用的属性,如下所示:

public class UnorderedFrozenAttribute : CustomizeAttribute
{
    public override ICustomization GetCustomization(ParameterInfo parameter)
    {
        return new CompositeCustomization(                
            new UnorderedSyncItems(),
            new FreezingCustomization(parameter.ParameterType));
    }
}

FreezingCustomization提供[Frozen]属性的底层实现。)

这使您能够编写此测试:

[Theory, AutoData]
public void DeclarativeTest(
    [UnorderedFrozen]SyncItem[] unorderedItems,
    SyncItemList sut)
{
    var expected = unorderedItems.OrderBy(si => si.Key);
    Assert.Equal(expected, sut);
}

请注意,此声明性测试使用了[AutoData]没有任何自定义的默认属性,因为加扰现在由[UnorderedFrozen]属性在参数级别应用。

这也将使您能够使用封装在[AutoData]-derived 属性中的一组(其他)测试套件范围的约定,并且仍然[UnorderedFrozen]用作选择加入机制。

于 2013-07-28T11:29:21.710 回答