对单元测试排序

有时,你可能希望按特定顺序运行单元测试。 理想情况下,单元测试的运行顺序不重要,最佳做法是避免对单元测试排序。 但无论如何,可能会有需要这样做。 为此,本文将演示如何对测试运行进行排序。

注意

测试排序和测试并行化是单独的问题。 指定执行顺序决定了测试启动的顺序,但如果启用了并行化,则仍可同时运行多个测试。 若要保证测试按指定顺序一次运行一次,还必须禁用并行化。

如果想要浏览源代码,请参阅 order .NET Core 单元测试示例存储库。

提示

除了本文中概述的排序功能之外,还考虑使用 Visual Studio 创建自定义播放列表作为替代方法。

按字母顺序排序

注意

默认情况下,MSTest 在类中按顺序运行测试。 如果在<Parallelize>文件中使用.runsettings设置来配置并行度,则不同类中的测试可以同时运行,排序仅影响每个类中的执行顺序。

MSTest 按测试类中定义的相同顺序发现测试。

在测试资源管理器(Visual Studio或Visual Studio Code)中运行时,测试按字母顺序排列,具体取决于测试名称。

在测试资源管理器外部运行时,测试按测试类中定义的顺序执行。

注意

名为 Test14 的测试将在 Test2 之前运行,即使数字 2 小于 14 也是如此。 这是因为测试名称排序使用的是测试的文本名称。

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MSTest.Project;

[TestClass]
public class ByAlphabeticalOrder
{
    public static bool Test1Called;
    public static bool Test2Called;
    public static bool Test3Called;

    [TestMethod]
    public void Test2()
    {
        Test2Called = true;

        Assert.IsTrue(Test1Called);
        Assert.IsFalse(Test3Called);
    }

    [TestMethod]
    public void Test1()
    {
        Test1Called = true;

        Assert.IsFalse(Test2Called);
        Assert.IsFalse(Test3Called);
    }

    [TestMethod]
    public void Test3()
    {
        Test3Called = true;

        Assert.IsTrue(Test1Called);
        Assert.IsTrue(Test2Called);
    }
}

从 MSTest 3.6 开始,新的 runsettings 选项允许在测试资源管理器和命令行中按测试名称运行测试。 若要启用此功能,请将 OrderTestsByNameInClass 设置添加到 runsettings 文件:

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>

  <MSTest>
    <OrderTestsByNameInClass>true</OrderTestsByNameInClass>
  </MSTest>

</RunSettings>

随机排序

在 MSTest 4.3 及更高版本中,按随机顺序运行测试,以显示测试之间的隐藏排序依赖关系。 若要启用随机顺序,请在 runsettings 文件中将 RandomizeTestOrder 设置设置为 true

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>

  <MSTest>
    <RandomizeTestOrder>true</RandomizeTestOrder>
  </MSTest>

</RunSettings>

若要使随机顺序可重现,请将 RandomTestOrderSeed 设置设置为整数种子。 然后,MSTest 在每次运行时都会按相同的顺序再次执行。 未设置种子时,MSTest 会在每次运行时生成一个新种子。 不要将 RandomizeTestOrderOrderTestsByNameInClass 组合使用,因为 MSTest 一次只应用一种排序模式。 有关详细信息,请参阅 配置 MSTest

xUnit 测试框架允许对测试运行顺序进行更细致的控制。 可以实现 ITestCaseOrdererITestCollectionOrderer 接口,以控制类或测试集合的测试用例的顺序。

注意

默认情况下,xUnit 并行运行测试类。 单个类中的测试始终按顺序运行,因此 ITestCaseOrderer 控制该类中的序列。 若要在所有类中禁用并行度,请在程序集级别应用 ,例如在项目中的任何源文件中应用

按测试用例的字母顺序排序

若要按其方法名称对测试用例排序,可以实现 ITestCaseOrderer 并提供排序机制。

using Xunit.Abstractions;
using Xunit.Sdk;

namespace XUnit.Project.Orderers;

public class AlphabeticalOrderer : ITestCaseOrderer
{
    public IEnumerable<TTestCase> OrderTestCases<TTestCase>(
        IEnumerable<TTestCase> testCases) where TTestCase : ITestCase =>
        testCases.OrderBy(testCase => testCase.TestMethod.Method.Name);
}

然后,在测试类中,使用 TestCaseOrdererAttribute 设置测试用例的顺序。

using Xunit;

namespace XUnit.Project;

[TestCaseOrderer(
    ordererTypeName: "XUnit.Project.Orderers.AlphabeticalOrderer",
    ordererAssemblyName: "XUnit.Project")]
public class ByAlphabeticalOrder
{
    public static bool Test1Called;
    public static bool Test2Called;
    public static bool Test3Called;

    [Fact]
    public void Test1()
    {
        Test1Called = true;

        Assert.False(Test2Called);
        Assert.False(Test3Called);
    }

    [Fact]
    public void Test2()
    {
        Test2Called = true;

        Assert.True(Test1Called);
        Assert.False(Test3Called);
    }

    [Fact]
    public void Test3()
    {
        Test3Called = true;

        Assert.True(Test1Called);
        Assert.True(Test2Called);
    }
}

按集合的字母顺序排序

若要按其显示名称对测试集合排序,可以实现 ITestCollectionOrderer 并提供排序机制。

using Xunit;
using Xunit.Abstractions;

namespace XUnit.Project.Orderers;

public class DisplayNameOrderer : ITestCollectionOrderer
{
    public IEnumerable<ITestCollection> OrderTestCollections(
        IEnumerable<ITestCollection> testCollections) =>
        testCollections.OrderBy(collection => collection.DisplayName);
}

由于测试集合可能会并行运行,因此必须使用 CollectionBehaviorAttribute 显式禁用集合的测试并行化。 然后,将实现指定到 TestCollectionOrdererAttribute

using Xunit;

// Need to turn off test parallelization so we can validate the run order
[assembly: CollectionBehavior(DisableTestParallelization = true)]
[assembly: TestCollectionOrderer(
    ordererTypeName: "XUnit.Project.Orderers.DisplayNameOrderer",
    ordererAssemblyName: "XUnit.Project")]

namespace XUnit.Project;

[Collection("Xzy Test Collection")]
public class TestsInCollection1
{
    public static bool Collection1Run;

    [Fact]
    public static void Test()
    {
        Assert.True(TestsInCollection2.Collection2Run);     // Abc
        Assert.True(TestsInCollection3.Collection3Run);     // Mno
        Assert.False(TestsInCollection1.Collection1Run);    // Xyz

        Collection1Run = true;
    }
}

[Collection("Abc Test Collection")]
public class TestsInCollection2
{
    public static bool Collection2Run;

    [Fact]
    public static void Test()
    {
        Assert.False(TestsInCollection2.Collection2Run);    // Abc
        Assert.False(TestsInCollection3.Collection3Run);    // Mno
        Assert.False(TestsInCollection1.Collection1Run);    // Xyz

        Collection2Run = true;
    }
}

[Collection("Mno Test Collection")]
public class TestsInCollection3
{
    public static bool Collection3Run;

    [Fact]
    public static void Test()
    {
        Assert.True(TestsInCollection2.Collection2Run);     // Abc
        Assert.False(TestsInCollection3.Collection3Run);    // Mno
        Assert.False(TestsInCollection1.Collection1Run);    // Xyz

        Collection3Run = true;
    }
}

按自定义属性排序

若要使用自定义属性对 xUnit 测试进行排序,首先需要一个可依赖的属性。 按如下所示定义 TestPriorityAttribute

namespace XUnit.Project.Attributes;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class TestPriorityAttribute : Attribute
{
    public int Priority { get; private set; }

    public TestPriorityAttribute(int priority) => Priority = priority;
}

接下来,考虑以下 PriorityOrderer 接口的 ITestCaseOrderer 实现。

using Xunit.Abstractions;
using Xunit.Sdk;
using XUnit.Project.Attributes;

namespace XUnit.Project.Orderers;

public class PriorityOrderer : ITestCaseOrderer
{
    public IEnumerable<TTestCase> OrderTestCases<TTestCase>(
        IEnumerable<TTestCase> testCases) where TTestCase : ITestCase
    {
        string assemblyName = typeof(TestPriorityAttribute).AssemblyQualifiedName!;
        var sortedMethods = new SortedDictionary<int, List<TTestCase>>();
        foreach (TTestCase testCase in testCases)
        {
            int priority = testCase.TestMethod.Method
                .GetCustomAttributes(assemblyName)
                .FirstOrDefault()
                ?.GetNamedArgument<int>(nameof(TestPriorityAttribute.Priority)) ?? 0;

            GetOrCreate(sortedMethods, priority).Add(testCase);
        }

        foreach (TTestCase testCase in
            sortedMethods.Keys.SelectMany(
                priority => sortedMethods[priority].OrderBy(
                    testCase => testCase.TestMethod.Method.Name)))
        {
            yield return testCase;
        }
    }

    private static TValue GetOrCreate<TKey, TValue>(
        IDictionary<TKey, TValue> dictionary, TKey key)
        where TKey : struct
        where TValue : new() =>
        dictionary.TryGetValue(key, out TValue? result)
            ? result
            : (dictionary[key] = new TValue());
}

然后,在测试类中,使用 TestCaseOrdererAttribute 将测试用例的顺序设置为 PriorityOrderer

using Xunit;
using XUnit.Project.Attributes;

namespace XUnit.Project;

[TestCaseOrderer(
    ordererTypeName: "XUnit.Project.Orderers.PriorityOrderer",
    ordererAssemblyName: "XUnit.Project")]
public class ByPriorityOrder
{
    public static bool Test1Called;
    public static bool Test2ACalled;
    public static bool Test2BCalled;
    public static bool Test3Called;

    [Fact, TestPriority(5)]
    public void Test3()
    {
        Test3Called = true;

        Assert.True(Test1Called);
        Assert.True(Test2ACalled);
        Assert.True(Test2BCalled);
    }

    [Fact, TestPriority(0)]
    public void Test2B()
    {
        Test2BCalled = true;

        Assert.True(Test1Called);
        Assert.True(Test2ACalled);
        Assert.False(Test3Called);
    }

    [Fact]
    public void Test2A()
    {
        Test2ACalled = true;

        Assert.True(Test1Called);
        Assert.False(Test2BCalled);
        Assert.False(Test3Called);
    }

    [Fact, TestPriority(-5)]
    public void Test1()
    {
        Test1Called = true;

        Assert.False(Test2ACalled);
        Assert.False(Test2BCalled);
        Assert.False(Test3Called);
    }
}

按优先级排序

注意

默认情况下,NUnit 在单个线程中按顺序运行测试。 当不需要应用[Parallelizable]属性时,仅应用[Order]属性就足以保证指定序列中的串行执行。

为了显式对测试排序,NUnit 提供了 OrderAttribute。 具有此属性的测试先于没有此属性的测试启动。 顺序值用于确定运行单元测试的顺序。

using NUnit.Framework;

namespace NUnit.Project;

public class ByOrder
{
    public static bool Test1Called;
    public static bool Test2ACalled;
    public static bool Test2BCalled;
    public static bool Test3Called;

    [Test, Order(5)]
    public void Test1()
    {
        Test1Called = true;

        Assert.That(Test2ACalled, Is.False);
        Assert.That(Test2BCalled, Is.True);
        Assert.That(Test3Called, Is.True);
    }

    [Test, Order(0)]
    public void Test2B()
    {
        Test2BCalled = true;

        Assert.That(Test1Called, Is.False);
        Assert.That(Test2ACalled, Is.False);
        Assert.That(Test3Called, Is.True);
    }

    [Test]
    public void Test2A()
    {
        Test2ACalled = true;

        Assert.That(Test1Called, Is.True);
        Assert.That(Test2BCalled, Is.True);
        Assert.That(Test3Called, Is.True);
    }

    [Test, Order(-5)]
    public void Test3()
    {
        Test3Called = true;

        Assert.That(Test1Called, Is.False);
        Assert.That(Test2ACalled, Is.False);
        Assert.That(Test2BCalled, Is.False);
    }
}

后续步骤