• XUnit数据共享与并行测试


    引言

    在单元或者集成测试的过程中,需要测试的用例非常多,如果测试是一条一条过,那么需要花费不少的时间。从 V2 开始,默认情况下 XUnit 自动配置并行(参考资料),大大提升了测试速度。本文将对 ASP.NET CORE WEBAPI 程序进行集成测试,并探讨 XUnit 的数据共享与测试并行的方法。

    XUnit默认在一个类内的测试代码是串行执行的,而在不同类的测试代码是并行执行的。

    集成测试

    对于集成测试来说,我们有一些比较重的资源初始化,而我并不想他们在并行执行中重复初始化,因此需要将并行执行的资源共享。

    我们现在的测试类是这样的:

        public class ProgramTests : IClassFixture>
        {
            private readonly WebApplicationFactory _factory;
            private readonly ITestOutputHelper testOutputHelper;
            private readonly HttpClient _client;
            public ProgramTests(WebApplicationFactory 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(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(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> 共享了必要的数据吗?

    并没有,XUnit 只是注入了 WebApplicationFactory,而我们在构造函数中执行了很多费时间的操作,包括构造 HttpClient ,获取 token 等。由于获取 token 的函数需要调用外部服务花费了很长的时间,我们可以尝试注入 HttpClient 进行优化。

    请注意:大多数情况注入 HttpClient 不是一个好主意,更推荐利用 WebApplicationFactory 对每个测试动态生成 HttpClient 以保证 HttpClient 是初始干净的状态。

    我们加入一个新的类:

        public class SharedHttpClientFixture : IDisposable
        {
            public HttpClient Client { get; init; }
    
            public SharedHttpClientFixture()
            {
                WebApplicationFactory 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
        {
            private readonly ITestOutputHelper testOutputHelper;
            private readonly HttpClient _client;
            public ProgramTests(SharedHttpClientFixture httpClientFixture, ITestOutputHelper testOutputHelper)
            {
                _client = httpClientFixture.Client;
                this.testOutputHelper = testOutputHelper;
            }
    

    改完之后,速度提升效果还是非常显著的:
    image

    跨类串行

    我们多数情况下不会将所有的测试都放在一个类中,对于多个类,我们需要跨类共享。XUnit 使用 ICollectionFixture<> 支持跨类共享。代码主体拆成两个类,并修改类签名如下:

        [Collection("V1 Test Fixture")]
        public class ProgramTests
        {
        ...
        }
        [Collection("V1 Test Fixture")]
        public class UploadTests
        {
    	....
    	}
    

    我们需要新定义一个类,这个类没有实质性作用,只是作为标识:

        [CollectionDefinition("V1 Test Fixture")]
        public class TestCollection : ICollectionFixture
        {
            // 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.
        }
    

    我们针对多个类进行测试:
    image

    跨类并行(数据不共享)

    我们注意到,不同类使用了相同的 Collection 进行标注,因此他们实际上会进行同步调度——上一个执行完成后才会开始执行下一个测试。我们如果使用并行会怎么样呢?显然,修改 Colleciton 会对每个类都生成一次需要注入对象,数据不能直接被共享。

        [Collection("V1 Test Fixture1")]
        public class ProgramTests
        {
        ...
        }
        [Collection("V1 Test Fixture")]
        public class UploadTests
        {
    	....
    	}
    	[CollectionDefinition("V1 Test Fixture1")]
        public class Test1Collection : ICollectionFixture
        {
            // 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
        {
            // 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.
        }
    

    初始化语句会被执行两次,我们实现了并行,但是数据并不是共享的。(大多数情况下已经够用了。)
    image

    跨类并行(数据共享)

    由于任务并行无法得知其他任务的工作状态,这个时候数据共享可能会引入很多线程问题(竞争、死锁等),因此不太建议在这种情况下进行共享,我最终也是使用的并行不共享的方式实现。如果我们非得这么用,也不是不行,我们需要小心处理线程同步问题,以互斥锁为例:

     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();
            }
    }
    

    这样多个类型使用静态变量实现了共享,并利用互斥锁保证初始化只执行一次。
    image

    由于引入了线程同步机制,这种情况下,并行测试并不一定意味着性能会更好,实际上往往还会更差。

    生命周期

    XUnit 对共享的数据类型执行以下策略:

    • IClassFixture,类的第一个测试方法执行之前,会对注入对象进行初始化。随后每一个方法都会生成测试的类的新对象,并将注入对象传递给他们,在测试类中所有测试方法执行完毕后销毁。
    • ICollectionFixture,多个类中执行的第一个测试方法之前会对注入对象进行初始化。随后每一个方法都会生成测试类的新对象,并且将注入对象传递给他们,在所有测试类的最后一个方法执行完毕之后销毁。

    Program 可见性

    默认情况下 Program 是其他项目不可见的,这样会导致 WebApplicationFactory 提示错误。.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 测试中,可以使用 IClassFixtureICollectionFixture 来进行数据共享,对于相同类之间的测试会默认进行的串行测试,对不同类之间共享数据的情况,也会进行串行调用。对于在不同类的测试,推荐使用不共享数据的并行测试,数据共享越多,造成状态不一致的风险就越大,因此建议有限制地使用测试数据共享。

    请注意,XUnit 不会统计注入对象的初始化时间,而且多次运行测试时间会有一些区别,因此本文中列出的时间仅供参考。

    拓展阅读

    如果觉得自带的方注入方式满足不了你的要求,那么可以考虑使用第三方类库实现的支持 XUnit 的依赖注入容器。请关注这个项目: pengweiqhca/Xunit.DependencyInjection: Use Microsoft.Extensions.DependencyInjection to resolve xUnit test cases. (github.com)

  • 相关阅读:
    数据结构和算法
    EDA实验------数控分频器设计(QuartusII)
    腾讯云Java后端15连问(6年经验):分布式+锁+MySQL+JVM+TCP
    【源码】16国语言交易所源码/币币交易+期权交易+秒合约交易+永续合约+交割合约+新币申购+投资理财/手机端uniapp纯源码+PC纯源码+后端PHP
    LeetCode高频题69. x 的平方根,二分法搞定,非常简单
    如何套用模板制作大屏?
    LM小型可编程控制器软件(基于CoDeSys)笔记二十:plc通过驱动器控制步进电机
    web网页设计期末课程大作业:美食餐饮文化主题网站设计——中华美德6页面HTML+CSS+JavaScript
    【C++】友元函数 ( 友元函数简介 | 友元函数声明 | 友元函数语法 | 友元函数声明不受访问控制限制 | 友元函数参数要求 )
    人工智能轨道交通行业周刊-第60期(2023.9.11-9.17)
  • 原文地址:https://www.cnblogs.com/podolski/p/17388602.html