如何检查列表是否有序?

71

我正在进行一些单元测试,并且我想知道是否有任何方法可以测试列表是否按其包含的对象的属性排序。

现在我是这样做的,但我不喜欢它,我想要一个更好的方法。请问有人可以帮帮我吗?

// (fill the list)
List<StudyFeedItem> studyFeeds = 
    Feeds.GetStudyFeeds(2120, DateTime.Today.AddDays(-200), 20);   

StudyFeedItem previous = studyFeeds.First();

foreach (StudyFeedItem item in studyFeeds)
{
    if (item != previous)
    {
        Assert.IsTrue(previous.Date > item.Date);
    }

    previous = item;
}

4
小心不要测试不需要测试的东西。你是在确保查询包含了预期的 order by 子句,还是仅仅检查 order by 子句是否起作用?后者是浪费时间的。 - Chris
3
@PITADev:是的,你应该完全相信数据库引擎和CLR能够保持事物正确。你不是在测试int x = 2, y = 2, z = x + y是否成功通过了Assert.IsTrue(z == 4)吧?你应该只对公共方法行为进行单元测试,仅此而已。所以,如果repository.GetItems(true)的预期行为是返回一个有序项目列表,则测试它。但不要测试items.OrderBy(x=>x,new YourComparer())是否确实对列表进行了排序。但是,请对YourComparer确实正确比较进行单元测试。 - jason
你确定它总是按升序或降序排序吗? - user1666620
不,这是我想在两种不同情境下测试的东西。 - Pavel Zagalsky
2
使用LINQ最简单的方法是 l.OrderBy(e => e).SequenceEqual(l) - a-ctor
显示剩余6条评论
22个回答

76

如果你正在使用MSTest,你可能想要看一下CollectionAssert.AreEqual

Enumerable.SequenceEqual是另一个在断言中可用的有用API。

在这两种情况下,你应该准备一个按照期望顺序包含期望列表的列表,然后将该列表与结果进行比较。

以下是一个示例:

var studyFeeds = Feeds.GetStudyFeeds(2120, DateTime.Today.AddDays(-200), 20);   
var expectedList = studyFeeds.OrderByDescending(x => x.Date);
Assert.IsTrue(expectedList.SequenceEqual(studyFeeds));

1
问题是:“有没有办法测试列表是否按其包含的对象的属性排序。”因此:一个测试,如果序列按给定属性排序,则返回true。不是:如果序列等于另一个序列。我会使用扩展方法,可能使用Enumerable.Aggregate - Harald Coppoolse

61

使用.NET 4.0的方法是使用Enumerable.Zip方法将列表与偏移一个位置的自身配对,这会将列表中的每个项与其后续项配对。然后你可以检查每对中条件是否为真,例如:

var ordered = studyFeeds.Zip(studyFeeds.Skip(1), (a, b) => new { a, b })
                        .All(p => p.a.Date < p.b.Date);

如果你使用的是较早版本的框架,你可以很容易地编写自己的Zip方法,类似于以下代码(参数验证和适用时的枚举器处理由读者自行完成):

public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(
    this IEnumerable<TFirst> first,
    IEnumerable<TSecond> second,
    Func<TFirst, TSecond, TResult> selector)
{
    var e1 = first.GetEnumerator();
    var e2 = second.GetEnumerator();
    while (e1.MoveNext() & e2.MoveNext()) // one & is important
        yield return selector(e1.Current, e2.Current);
}

2
这种方法比使用OrderBy()的其他答案更节省内存。 - DuckMaestro
3
这需要置于顶部。 - nawfal
这可以进一步简化,使用一个lambda表达式而不是两个,并且摆脱匿名类型,提高可读性。请参见我的答案。 :) - AnorZaken
值得修复的一件事是条件应该是<=。在集合中相邻的具有相同日期的项目不会使顺序无效。 - Max

37

Nunit 2.5引入了CollectionOrderedContraint并提供了一种良好的语法来验证集合的顺序:

Assert.That(collection, Is.Ordered.By("PropertyName"));

无需手动排序和比较。


我喜欢这个,但是它目前的状态非常有限(不能按多列排序并设置升序/降序。:( - Erik Philips
Nunit的功能已得到拓展。如你所见(在手册中),现在断言允许使用自定义比较器。约束支持多种设置,包括所需属性的方向和选择(甚至包括多个属性)。 - Jan

27

如果您的单元测试框架有用于比较集合相等性的助手方法,您应该能够像这样做(以NUnit为例):

var sorted = studyFeeds.OrderBy(s => s.Date);
CollectionAssert.AreEqual(sorted.ToList(), studyFeeds.ToList());

assert 方法适用于任何 IEnumerable,但当两个集合都是 IList 或 "数组" 类型时,当断言失败时抛出的错误消息将包含第一个不在正确位置的元素的索引。


+1。很好。我已经使用NUnit多年了,但是不知道这个。我也刚刚发现了FileAssert和StringAssert!谢谢。 - RichardOD
它不起作用 :(. 错误信息:Baischana.Components.Services.Test.StudyFeedsTest.GetStudyFeeds_IsOrderedByDate: 预期: <System.Linq.OrderedEnumerable`2[Baischana.Components.StudyFeedItem,System.DateTime]> 实际: < <Baischana.Components.StudyFeedItem>, <Baischana.Components.StudyFeedItem>, <Baischana.Components.StudyFeedItem>, <Baischana.Components.StudyFeedItem>, <Baischana.Components.StudyFeedItem> > - Nicole
1
按照计算复杂度来衡量,对列表进行排序相当低效。 - el.pescado - нет войне
7
+1 for CollectionAssert,我不知道这个。@el-pescado:没错,但这是单元测试,不是生产代码... - Thomas Levesque
@Nicole:当断言失败时,您会收到该错误消息。尝试将assert方法传递两个类型为IList的对象(我的答案已编辑为此),它应该会给您另一个错误消息,其中包含第一个不在正确位置的元素的索引。 - Jørn Schou-Rode

21

涉及对列表进行排序的发布的解决方案代价高昂 - 确定列表是否已排序可以在O(N)内完成。以下是一个扩展方法,可用于检查:

public static bool IsOrdered<T>(this IList<T> list, IComparer<T> comparer = null)
{
    if (comparer == null)
    {
        comparer = Comparer<T>.Default;
    }

    if (list.Count > 1)
    {
        for (int i = 1; i < list.Count; i++)
        {
            if (comparer.Compare(list[i - 1], list[i]) > 0)
            {
                return false;
            }
        }
    }
    return true;
}

通过将> 0更改为< 0,可以轻松地实现相应的IsOrderedDescending


>=是错误的,它会在[1, 1, 1, 2, 3]上返回false。修改以检测反向排序也很容易。 - Richard
3
比那种排序数据的方法要好得多,(这让我牙痒痒)。 - Matthew Watson
1
你不能在泛型方法内部使用 > 或任何运算符来比较元素。 - Sriram Sakthivel
1
可以使用 IList<String>,但无法使用 IList<T> 或者 IEnumerable<T> - Tim Schmelter
确实,我应该在VS中编写而不是在浏览器中!使用了你所用的相同比较器方法进行更新。 - Richard
你甚至不需要列表或索引访问。你可以在简单的IEnumerable上完成它。 - Sedat Kapanoglu

14

Greg Beech的回答虽然很好,但可以通过在Zip本身中进行测试来进一步简化。因此,不是:

var ordered = studyFeeds.Zip(studyFeeds.Skip(1), (a, b) => new { a, b })
                        .All(p => p.a.Date <= p.b.Date);

你可以简单地执行以下操作:

var ordered = !studyFeeds.Zip(studyFeeds.Skip(1), (a, b) => a.Date <= b.Date)
                        .Contains(false);

这样可以省去一个lambda表达式和一个匿名类型。

(根据我的观点,移除匿名类型也使代码更易读。)


1
我发现这个版本更容易阅读: studyFeeds.Zip(studyFeeds.Skip(1), (a, b) => a.Date < b.Date) .All(b => true); - StackHola
就我个人而言,我无法感知在测试所有内容是否为真与测试任何一个是否为假之间的任何清晰度差异,对我来说,额外的lambda似乎是不必要的,但无论如何,我猜这取决于你自己。 - AnorZaken
5
@StackHola,我认为您打错了一个字: (b => true)无论b的值是假还是真,都会返回真,我认为您想写的是(b => b)。请确认一下。 - Steve Cadwallader
@SteveCadwallader 是的,你说得对,无法编辑回去。正确的形式是:studyFeeds.Zip(studyFeeds.Skip(1), (a, b) => a.Date < b.Date).All(b => b); - StackHola
由于现在读作“所有 b”而不是“所有真实”,我认为它并不更容易阅读。 当您读取 All(b => b) 时,您会停顿一下,想一下我们正在进行一个“全为真”的测试,然后必须重新阅读前面的部分以查看我们要测试的实际条件。 - AnorZaken
1
值得修复的一件事是条件应该是<=而不是<。相邻的相同值并不会使顺序无效。 - Max

9
if(studyFeeds.Length < 2)
  return;

for(int i = 1; i < studyFeeds.Length;i++)  
 Assert.IsTrue(studyFeeds[i-1].Date > studyFeeds[i].Date);

for并没有完全消失!


8

怎么样:

var list = items.ToList();
for(int i = 1; i < list.Count; i++) {
    Assert.IsTrue(yourComparer.Compare(list[i - 1], list[i]) <= 0);
} 

其中yourComparer是一个实现了IComparer<YourBusinessObject>接口的YourComparer实例。这确保了枚举中的每个元素都小于下一个元素。


FYI:合并自http://stackoverflow.com/questions/1676178/simple-sort-verification-for-unit-testing-an-order-by - Shog9
断言只在调试时有用,它会告诉你列表是否有序,但在发布时不会做任何事情。当某些东西应该始终为真时,断言对于验证很有用! - Romerik Rousseau

6

Linq的答案是:

您可以使用SequenceEqual方法来检查原始序列和排序后的序列是否相同。

var isOrderedAscending = lJobsList.SequenceEqual(lJobsList.OrderBy(x => x));
var isOrderedDescending = lJobsList.SequenceEqual(lJobsList.OrderByDescending(x => x));

不要忘记导入 System.Linq 命名空间。

此外:

我再次强调,本答案基于 Linq,您可以通过创建自定义扩展方法实现更高效的操作。

另外,如果有人仍然想使用 Linq 并检查序列是否按升序或降序排序,则可以像这样实现略微更高效的操作:

var orderedSequence = lJobsList.OrderBy(x => x)
                               .ToList();

var reversedOrderSequence = orderedSequence.AsEnumerable()
                                           .Reverse();

if (lJobsList.SequenceEqual(orderedSequence))
{
     // Ordered in ascending
}
else (lJobsList.SequenceEqual(reversedOrderSequence))
{
     // Ordered in descending
}

@PavelZagalsky 很高兴能帮到你。但是别忘了还有其他更有效的方法需要你自己实现。 - Farhad Jabiyev
1
@Downvoter,您能否解释一下为什么要给我点踩呢? - Farhad Jabiyev
有时候我不理解人们 :( - Pavel Zagalsky
1
@PavelZagalsky :) 有些人只会伤害,却没有治愈的能力。 - Farhad Jabiyev
1
我再重申一遍,Linq和扩展方法与性能无关。 - CodeCaster
显示剩余6条评论

6
您可以使用以下扩展方法:

public static System.ComponentModel.ListSortDirection? SortDirection<T>(this IEnumerable<T> items, Comparer<T> comparer = null)
{
    if (items == null) throw new ArgumentNullException("items");
    if (comparer == null) comparer = Comparer<T>.Default;

    bool ascendingOrder = true; bool descendingOrder = true;
    using (var e = items.GetEnumerator())
    {
        if (e.MoveNext())
        {
            T last = e.Current; // first item
            while (e.MoveNext())
            {
                int diff = comparer.Compare(last, e.Current);
                if (diff > 0)
                    ascendingOrder = false;
                else if (diff < 0)
                    descendingOrder = false;

                if (!ascendingOrder && !descendingOrder)
                    break;
                last = e.Current;
            }
        }
    }
    if (ascendingOrder)
        return System.ComponentModel.ListSortDirection.Ascending;
    else if (descendingOrder)
        return System.ComponentModel.ListSortDirection.Descending;
    else
        return null;
}

它可以检查序列是否已排序并确定方向:

var items = new[] { 3, 2, 1, 1, 0 };
var sort = items.SortDirection();
Console.WriteLine("Is sorted? {0}, Direction: {1}", sort.HasValue, sort);
//Is sorted? True, Direction: Descending

这比我预期的一个自包含的LINQ函数要多次迭代输入。 - Rawling
1
@Rawling:因为使用了 AnyFirst。这并不会迭代整个序列,而只是检查第一个元素。如果任何一个元素没有排序,它就会停止枚举并返回 false - Tim Schmelter
它仍然调用 GetEnumerable。如果这很昂贵,而且它确实很昂贵,那么你会比你需要的多调用三次。 - Rawling
@Rawling:确实,被采纳的答案更加优雅。但是 OrderBy + SequenceEqual 不是更低效吗?因为它需要在开始使用 SequenceEqual 之前对整个序列进行排序,而我的 foreach 可以立即比较顺序。此外,它还需要处理两次以检查两个方向,而我的循环只需要一次。 - Tim Schmelter
1
@TimSchmelter 你的答案比被接受的答案好多了。但我个人更喜欢使用隐式枚举器(using(var e = items.GetEnumerator())e.MoveNext()e.Current)来避免使用Any()SkipFirst - Ivan Stoev
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接