如何设置确定性测试以验证列表中的项目是否已订购?
首先,我做了以下事情:
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>
继承。
答案 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]
作为选择加入机制。