XUnit数据共享与并行测试
引言
在单元或者集成测试的过程中,需要测试的用例非常多,如果测试是一条一条过,那么需要花费不少的时间。从 V2 开始,默认情况下 XUnit 自动配置并行(参考资料),大大提升了测试速度。本文将对 ASP.NET CORE WEBAPI 程序进行集成测试,并探讨 XUnit 的数据共享与测试并行的方法。
XUnit默认在一个类内的测试代码是串行执行的,而在不同类的测试代码是并行执行的。
集成测试
对于集成测试来说,我们有一些比较重的资源初始化,而我并不想他们在并行执行中重复初始化,因此需要将并行执行的资源共享。
我们现在的测试类是这样的:
public class ProgramTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly ITestOutputHelper testOutputHelper;
private readonly HttpClient _client;
public ProgramTests(WebApplicationFactory<Program> factory, ITestOutputHelper testOutputHelper)
{
_factory = factory;
this.testOutputHelper = testOutputHelper;
_client = _factory.WithWebHostBuilder(builder =>
{
builder.UseEnvironment(Environments.Production);
}).CreateClient(new WebApplicationFactoryClientOptions() { BaseAddress = new Uri("http://localhost:9000") });
var token = TokenHelper.GetToken("username", "password");
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Act
}
[Fact]
public async Task V1Legacy_GetDeviceInfoes()
{
string url = "url1";
// Arrange
testOutputHelper.WriteLine($"Testing:{url}");
var response = await _client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
var result = await response.Content.ReadAsStringAsync();
var target = JsonSerializer.Deserialize<DeviceInfo>(result, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
Assert.NotNull(target);
}
[Fact]
public async Task V1Legacy_GetCurrent()
{
var url = "url2";
// Arrange
testOutputHelper.WriteLine($"Testing:{url}");
var response = await _client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
var result = await response.Content.ReadAsStringAsync();
var target = JsonSerializer.Deserialize<DeviceDataDto>(result, new JsonSerializerOptions {PropertyNameCaseInsensitive = true });
Assert.NotNull(target);
}
[Theory]
[InlineData("url3")]
[InlineData("url4")]
public async Task V1Legacy_CheckUrlExist(string url)
{
// Arrange
testOutputHelper.WriteLine($"Testing:{url}");
var request = new HttpRequestMessage(HttpMethod.Head, url);
var response = await _client.SendAsync(request);
// Assert
Assert.NotEqual(404, (int)response.StatusCode);
}
}
在这个测试中,使用 IClassFixture 进行集成测试,确保同一个类之内的代码共享同一个资源,不同测试方法串行执行。
TIPS: 这里我使用 HEAD 请求来探查给定地址是否存在,ASP. NET CORE 会默认拒绝这个请求(返回406),但是不会提示 404 的错误。
现在的运行时间是这样的:

单类优化
首先研究为什么这个程序花费了如此多的时间执行测试,XUnit 在进行不同 Fact 的测试时,会生成不同的对象,我们已经通过实现 IClassFixture<WebApplicationFactory<Program>> 共享了必要的数据吗?
并没有,XUnit 只是注入了 WebApplicationFactory<Program>,而我们在构造函数中执行了很多费时间的操作,包括构造 HttpClient ,获取 token 等。由于获取 token 的函数需要调用外部服务花费了很长的时间,我们可以尝试注入 HttpClient 进行优化。
请注意:大多数情况注入 HttpClient 不是一个好主意,更推荐利用
WebApplicationFactory<Program>对每个测试动态生成 HttpClient 以保证 HttpClient 是初始干净的状态。
我们加入一个新的类:
public class SharedHttpClientFixture : IDisposable
{
public HttpClient Client { get; init; }
public SharedHttpClientFixture()
{
WebApplicationFactory<YourAssemblyName.Program> factory = new();
Client = factory.WithWebHostBuilder(builder =>
{
builder.UseEnvironment(Environments.Production);
}).CreateClient(new WebApplicationFactoryClientOptions() { BaseAddress = new Uri("http://localhost:9000") });
var token = TokenHelper.GetToken("username", "password");
Client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Act
}
public void Dispose()
{
//throw new NotImplementedException();
}
}
并修改测试类的签名:
public class ProgramTests : IClassFixture<SharedHttpClientFixture>
{
private readonly ITestOutputHelper testOutputHelper;
private readonly HttpClient _client;
public ProgramTests(SharedHttpClientFixture httpClientFixture, ITestOutputHelper testOutputHelper)
{
_client = httpClientFixture.Client;
this.testOutputHelper = testOutputHelper;
}
改完之后,速度提升效果还是非常显著的:

跨类
我们多数情况下不会将所有的测试都放在一个类中,对于多个类,我们需要跨类共享。XUnit 使用 ICollectionFixture<> 支持跨类共享。代码主体拆成两个类,并修改类签名如下:
[Collection("V1 Test Fixture")]
public class ProgramTests
{
...
}
[Collection("V1 Test Fixture")]
public class UploadTests
{
....
}
我们需要新定义一个类,这个类没有实质性作用,只是作为标识:
[CollectionDefinition("V1 Test Fixture")]
public class TestCollection : ICollectionFixture<SharedHttpClientFixture>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}
我们针对多个类进行测试:

跨类并行(数据不共享)
我们注意到,不同类使用了相同的 Collection 进行标注,因此他们实际上会进行同步调度——上一个执行完成后才会开始执行下一个测试。我们如果使用并行会怎么样呢?显然,修改 Colleciton 会对每个类都生成一次需要注入对象,数据不能直接被共享。
[Collection("V1 Test Fixture1")]
public class ProgramTests
{
...
}
[Collection("V1 Test Fixture")]
public class UploadTests
{
....
}
[CollectionDefinition("V1 Test Fixture1")]
public class Test1Collection : ICollectionFixture<SharedHttpClientFixture>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}
[CollectionDefinition("V1 Test Fixture")]
public class TestCollection : ICollectionFixture<SharedHttpClientFixture>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}
初始化语句会被执行两次,我们实现了并行,但是数据并不是共享的。(大多数情况下已经够用了。)

跨类并行(数据共享)
由于任务并行无法得知其他任务的工作状态,这个时候数据共享可能会引入很多线程问题(竞争、死锁等),因此不太建议在这种情况下进行共享,我最终也是使用的并行不共享的方式实现。如果我们非得这么用,也不是不行,我们需要小心处理线程同步问题,以互斥锁为例:
public class SharedHttpClientFixture : IDisposable
{
private static HttpClient _httpClient;
public HttpClient Client => GetClient();
private HttpClient GetClient()
{
if (_httpClient == null) Init();
return _httpClient;
}
public static Mutex count = new();
public SharedHttpClientFixture()
{
}
private void Init()
{
count.WaitOne();
if(_httpClient == null)
{
...
}
count.ReleaseMutex();
}
public void Dispose()
{
//throw new NotImplementedException();
}
}
这样多个类型使用静态变量实现了共享,并利用互斥锁保证初始化只执行一次。

由于引入了线程同步机制,这种情况下,并行测试并不一定意味着性能会更好,实际上往往还会更差。
生命周期
XUnit 对共享的数据类型执行以下策略:
- 对
IClassFixture,类的第一个测试方法执行之前,会对注入对象进行初始化。随后每一个方法都会生成测试的类的新对象,并将注入对象传递给他们,在测试类中所有测试方法执行完毕后销毁。 - 对
ICollectionFixture,多个类中执行的第一个测试方法之前会对注入对象进行初始化。随后每一个方法都会生成测试类的新对象,并且将注入对象传递给他们,在所有测试类的最后一个方法执行完毕之后销毁。
Program 可见性
默认情况下 Program 是其他项目不可见的,这样会导致 WebApplicationFactory<Program> 提示错误。.NET 6 开始引入了 minimal API,我的项目是升级而来,并没有使用到这个东西,所以 Program 类是对外可见的。
public class Program
{
static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://*:9000");
ConfigureServices(builder.Services, builder.Configuration);
var app = builder.Build();
Configure(app);
app.Run();
}
}
如果使用 Minimal API,那么你需要在项目文件中对测试项目公开可见性。
<ItemGroup>
<InternalsVisibleTo Include="MyTestProject" />
</ItemGroup>
或者在 Program.cs 的最后加上一行。
var builder = WebApplication.CreateBuilder(args);
// ... Configure services, routes, etc.
app.Run();
+ public partial class Program { }
注意事项
在 Microsoft.VisualStudio.TestPlatform.TestHost 命名空间也有一个 Program 类,如果你自己实现自定义类型,由于默认引用,不注意就使用了这个东西,而不是你 API 的 Program 类,这样会导致测试无法运行,提示:“找不到 testHost.dep.json”这样的错误,所以尽量使用带命名空间的限定名称。
结论
在 XUnit 测试中,可以使用 IClassFixture 与 ICollectionFixture 来进行数据共享,对于相同类之间的测试会默认进行的串行测试,对不同类之间共享数据的情况,也会进行串行调用。对于在不同类的测试,推荐使用不共享数据的并行测试,数据共享越多,造成状态不一致的风险就越大,因此建议有限制地使用测试数据共享。
请注意,XUnit 不会统计注入对象的初始化时间,而且多次运行测试时间会有一些区别,因此本文中列出的时间仅供参考。
拓展阅读
如果觉得自带的方注入方式满足不了你的要求,那么可以考虑使用第三方类库实现的支持 XUnit 的依赖注入容器。请关注这个项目: pengweiqhca/Xunit.DependencyInjection: Use Microsoft.Extensions.DependencyInjection to resolve xUnit test cases. (github.com)
XUnit数据共享与并行测试的更多相关文章
- U3D协程Coroutine之WWW与Update()的并行测试
using System.Collections; using UnityEditor; using UnityEngine; using UnityEngine.UI; /************* ...
- TestNG并行测试
并行(多线程)技术在软件术语里被定义为软件.操作系统或者程序可以并行地执行另外一段程序中多个部分或者子组件的能力.TestNG允许我们以并行(多线程)的方式来执行测试.这就意味着基于TestNG测试组 ...
- appium多机并行测试
在实际应用中需要对多个机型并行测试,节省时间 多机测试的思路 启动多个appium server与多台机器交互(android和ios均可) 注意:一定要使用node安装appium的命令行,使用 ...
- 为什么并行测试很困难以及如何使用 ConTest 辅助测试
众所周知并行程序设计易于产生 bug.更为严重的是,往往在开发过程的晚期当这些并行 bug 引起严重的损害时才能发现它们并且难于调试它们.即使彻底地对它们进行了调试,常规的单元测试实践也很可能遗漏并行 ...
- Windows Ping | Tracert 's Bat 脚本并行测试
系统:windows 需求:测试多台PC输出三个网站并行ping.tracert结果,多台PC同时进行. 说明:以www.baidu.com.www.sina.com.cn.www.tencent.c ...
- Selenium多浏览器并行测试
如果需要同时在IE.firefox.chrome进行测试,可以使用grid. Selenium Grid是一个智能代理服务器,允许Selenium测试将命令路由到远程Web浏览器实例.其目的是提供一种 ...
- 单元测试-xUnit总结
xUnit总结 什么是xUnit xUnit.net是针对.NET Framework的免费,开源,以社区为中心的单元测试工具. 自动化测试的优点 可以频繁的进行测试 可以在任何时间进行测试,也可以按 ...
- Selenium 4 Python的最佳测试框架
随着Python语言的使用越来越流行,基于Python的测试自动化框架也越来越流行.在项目选择最佳框架时,开发人员和测试人员会有些无法下手.做出选择是应该判断很多事情,框架的脚本质量,测试用例的简单性 ...
- XUnit - Shared Context between Tests
原文 单元测试类通常都会有share setup和cleanup的相关代码.xUnit.net根据共享的范围提供了几种share setup和cleanup的方法. Constructor and D ...
- C#~异步编程再续~大叔所理解的并行编程(Task&Parallel)
返回目录 并行这个概念出自.net4.5,它被封装在System.Threading.Tasks命名空间里,主要提供一些线程,异步的方法,或者说它是对之前Thread进行的二次封装,为的是让开发人员更 ...
随机推荐
- Hadoop警告信息:WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform.
when键入命令: hadoop fs -ls / 若出现以下警告信息: Hadoop警告问题:WARN util.NativeCodeLoader: Unable to load native-ha ...
- 解决vue项目中文出现乱码
出现中文乱码的文件以ediplus打开,右上方有点击file选择sall As....另存为 下方有encoding 选择utf-8 ,覆盖以前的文件,就好了.
- python+appium拉起APP
1.首先需要完成环境配置: JDK:https://www.cnblogs.com/wenlongma/p/17103062.html: SDK:https://www.cnblogs.com/w ...
- 线程Thread小记
1 public class ConcurentDemo extends Thread { 2 @Override 3 public void run() { 4 super.run(); 5 Sys ...
- 基于leaflet地图可视化(一)
最近,在学习地图可视化是基于公司的项目.但公司在项目上居然用图片来代替.无语~~~项目效果图(第一版)如下: 突发奇想,2016年自己就接触过地图可视化.但那是没有深入研究.只会用R语言来实现点基础. ...
- ServletConfig接口介绍
前言: Servlet 容器初始化 Servlet 时,会为这个 Servlet 创建一个 ServletConfig 对象,并将 ServletConfig 对象作为参数传递给 Servlet .通 ...
- c++ 内存顺序
搞懂无锁编程的重要一步是完全理解内存顺序! 本教程由作者和ChatGPT通力合作完成. 都有哪几种? c++的内存模型共有6种 memory_order_relaxed memory_order_co ...
- Spring--AOP简介+入门案例
AOP简介 面向切面编程:在不惊动原始设计的基础上,进行功能增强 各个要应用该功能的对象叫做连接点,那个功能叫做通知,表面上的代码没有发生变化,私下里发生变化的连接点,会出现切入点,切入点与通知通过切 ...
- 从开源模型、框架到自研,声网 Web 端虚拟背景算法正式发布
根据研究发现,在平均 38 分钟的视频会议里面,大概会有 13 分钟左右的时间用于处理和干扰相关的事情.同时研究也表明在参加在线会议的时候,人们更加倾向于语音会议,其中一个关键原因就是大家不希望个人隐 ...
- python + BeautifulSoup + selenium 实现爬取中医智库的古籍分类的数据
爬取内容为 该图片下的七个分类, 然后对应的每个种类的书本信息(摘要和目录) 效果为 代码如下 import requests from bs4 import BeautifulSoup import ...