XUnit数据共享与并行测试

科技资讯 投稿 6400 0 评论

XUnit数据共享与并行测试

引言

ASP.NET CORE WEBAPI 程序进行集成测试,并探讨 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, (intresponse.StatusCode; 
        }
    }

在这个测试中,使用 IClassFixture 进行集成测试,确保同一个类之内的代码共享同一个资源,不同测试方法串行执行。

现在的运行时间是这样的:

单类优化

IClassFixture<WebApplicationFactory<Program>> 共享了必要的数据吗?

WebApplicationFactory<Program>,而我们在构造函数中执行了很多费时间的操作,包括构造 HttpClient,获取 token 等。由于获取 token 的函数需要调用外部服务花费了很长的时间,我们可以尝试注入 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;
        }

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

跨类串行

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 可见性

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”这样的错误,所以尽量使用带命名空间的限定名称。

结论

IClassFixture 与 ICollectionFixture 来进行数据共享,对于相同类之间的测试会默认进行的串行测试,对不同类之间共享数据的情况,也会进行串行调用。对于在不同类的测试,推荐使用不共享数据的并行测试,数据共享越多,造成状态不一致的风险就越大,因此建议有限制地使用测试数据共享。

拓展阅读

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

编程笔记 » XUnit数据共享与并行测试

赞同 (27) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽