单元测试列表中项目的顺序

时间:2013-07-25 19:55:29

标签: autofixture

如何设置确定性测试以验证列表中的项目是否已订购?

首先,我做了以下事情:

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>继承。

1 个答案:

答案 0 :(得分:11)

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

势在必行的版本

这是一个比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);
}

虽然the default number of many items is 3,但我认为最好在此测试用例中明确地将其调出。这里使用的加扰算法利用了排序三个(不同的)元素序列之后的事实,将第一个元素移动到后面必须导致无序列表。

然而,这种方法的问题在于它依赖于变异fixture,因此很难重构为更具声明性的方法。

自定义版本

为了重构更具声明性的版本,您可以先将加扰算法封装在a 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();
        }
    }
}

解析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);
}

请注意使用Freeze方法。这是必要的,因为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][Frozen]属性。

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

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

声明测试用例

如果您可以简单地应用参数级属性,就像[Frozen]属性一样 - 可能将它们组合起来,如[Unordered][Frozen],这样会很好。但是,这种方法不起作用。

请注意前面的示例中顺序很重要。您必须在冻结前应用UnorderedSyncItems,否则,被冻结的阵列可能无法保证无序。

[Unordered][Frozen]参数级属性的问题在于,当编译时,.NET框架不保证AutoFixture xUnit.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]派生属性中的一组(其他)测试套件范围的约定,并仍然使用[UnorderedFrozen]作为选择加入机制。