如何让并行xUnit测试的每个测试类获取唯一测试数据?
问题
我有一个包含单个测试方法的xUnit测试类,原本用于加载包含测试描述的文件,现在希望将这些测试用例并行执行。
由于xUnit测试类的测试方法始终串行运行,我通过定义子类继承测试方法的方式实现了并行执行,这部分生效了。
但问题在于每个测试类都会独立加载测试数据,目前我尝试通过线程安全的方式让每个测试方法获取唯一的测试项,编写了如下代码:
using System.Collections.Concurrent; using Xunit.Abstractions; [assembly: CollectionBehavior(MaxParallelThreads = 10)] namespace MultiTest; public class Data { public static readonly ConcurrentStack<string[]> stack = new ConcurrentStack<string[]>(); static Data() { List<string[]> strings = new List<string[]>() { new string[] { "one" }, new string[] { "two" }, new string[] { "three" }, new string[] { "four" }, new string[] { "five" }, new string[] { "six" }, new string[] { "seven" }, new string[] { "eight" }, new string[] { "nine" }, new string[] { "ten" } }; foreach (var str in strings) stack.Push(str); } public static IEnumerable<object[]> GetTestData() { foreach (var str in stack) yield return new object[] { str }; } } public abstract class UnitTest1 { private readonly ITestOutputHelper _testOutputHelper; public UnitTest1(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } public static IEnumerable<object[]> GetTestData() => Data.GetTestData(); [Theory] [MemberData(nameof(GetTestData))] public void Test1(string arg) { _testOutputHelper.WriteLine($"Start: {arg}"); Thread.Sleep(1000); _testOutputHelper.WriteLine($"Done"); } } public class TestA : UnitTest1 { public TestA(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } } public class TestB : UnitTest1 { public TestB(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } } public class TestC : UnitTest1 { public TestC(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } } public class TestD : UnitTest1 { public TestD(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } }
当前代码运行后,10个测试用例每个都会被执行4次,因为每个测试类都会从迭代器获取相同的全部数据。如何确保在并行环境中,每个测试类实例的测试方法都能从测试数据源获取唯一的测试项?
解决方案
核心问题在于GetTestData方法只是遍历整个ConcurrentStack,没有实现取走并移除元素的逻辑,导致每个测试类都会拿到全部10条数据。要实现每个测试用例获取唯一测试项,需要修改数据获取逻辑,结合线程安全的弹出操作:
修改思路
- 替换数据获取逻辑:用
ConcurrentStack.TryPop方法从栈中取出元素,一旦取出就从栈中移除,确保每个测试项只会被获取一次。 - 简化数据存储:去掉不必要的字符串数组包装,直接存储字符串,减少冗余。
- 调整迭代器逻辑:通过循环尝试弹出元素,直到栈为空,让xUnit自动遍历所有可用的唯一测试项。
修改后的完整代码
using System.Collections.Concurrent; using Xunit.Abstractions; [assembly: CollectionBehavior(MaxParallelThreads = 10)] namespace MultiTest; public class Data { private static readonly ConcurrentStack<string> _testItems = new ConcurrentStack<string>(); static Data() { var items = new List<string> { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" }; foreach (var item in items) _testItems.Push(item); } public static IEnumerable<object[]> GetUniqueTestData() { while (_testItems.TryPop(out var item)) { yield return new object[] { item }; } } } public abstract class UnitTest1 { protected readonly ITestOutputHelper _testOutputHelper; public UnitTest1(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } public static IEnumerable<object[]> GetTestData() => Data.GetUniqueTestData(); [Theory] [MemberData(nameof(GetTestData))] public void Test1(string arg) { _testOutputHelper.WriteLine($"Start: {arg}"); Thread.Sleep(1000); _testOutputHelper.WriteLine($"Done: {arg}"); } } public class TestA : UnitTest1 { public TestA(ITestOutputHelper helper) : base(helper) { } } public class TestB : UnitTest1 { public TestB(ITestOutputHelper helper) : base(helper) { } } public class TestC : UnitTest1 { public TestC(ITestOutputHelper helper) : base(helper) { } } public class TestD : UnitTest1 { public TestD(ITestOutputHelper helper) : base(helper) { } }
关键说明
- 线程安全的弹出操作:
ConcurrentStack.TryPop是原子操作,确保多个并行测试类不会拿到同一个测试项,弹出后元素从栈中永久移除,避免重复执行。 - 动态测试用例生成:迭代器通过
while循环动态获取剩余的测试项,xUnit会自动识别所有可用的测试用例,无需手动指定数量。 - 并行执行分配:4个测试类会并行从栈中获取不同的测试项,最终10个测试用例各执行一次,不会重复。
内容的提问来源于Stack Exchange,提问作者Matt W
相关产品推荐
相关产品推荐

