翼度科技»论坛 编程开发 .net 查看内容

XUnit数据共享与并行测试

3

主题

3

帖子

9

积分

新手上路

Rank: 1

积分
9
引言

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

对于集成测试来说,我们有一些比较重的资源初始化,而我并不想他们在并行执行中重复初始化,因此需要将并行执行的资源共享。
我们现在的测试类是这样的:
  1.     public class ProgramTests : IClassFixture<WebApplicationFactory<Program>>
  2.     {
  3. <ItemGroup>
  4.      <InternalsVisibleTo Include="MyTestProject" />
  5. </ItemGroup>   private readonly WebApplicationFactory<Program> _factory;
  6. <ItemGroup>
  7.      <InternalsVisibleTo Include="MyTestProject" />
  8. </ItemGroup>   private readonly ITestOutputHelper testOutputHelper;
  9. <ItemGroup>
  10.      <InternalsVisibleTo Include="MyTestProject" />
  11. </ItemGroup>   private readonly HttpClient _client;
  12. <ItemGroup>
  13.      <InternalsVisibleTo Include="MyTestProject" />
  14. </ItemGroup>   public ProgramTests(WebApplicationFactory<Program> factory, ITestOutputHelper testOutputHelper)
  15. <ItemGroup>
  16.      <InternalsVisibleTo Include="MyTestProject" />
  17. </ItemGroup>   {
  18. <ItemGroup>
  19.      <InternalsVisibleTo Include="MyTestProject" />
  20. </ItemGroup><ItemGroup>
  21.      <InternalsVisibleTo Include="MyTestProject" />
  22. </ItemGroup>  _factory = factory;
  23. <ItemGroup>
  24.      <InternalsVisibleTo Include="MyTestProject" />
  25. </ItemGroup><ItemGroup>
  26.      <InternalsVisibleTo Include="MyTestProject" />
  27. </ItemGroup>  this.testOutputHelper = testOutputHelper;
  28. <ItemGroup>
  29.      <InternalsVisibleTo Include="MyTestProject" />
  30. </ItemGroup><ItemGroup>
  31.      <InternalsVisibleTo Include="MyTestProject" />
  32. </ItemGroup>  _client = _factory.WithWebHostBuilder(builder =>
  33. <ItemGroup>
  34.      <InternalsVisibleTo Include="MyTestProject" />
  35. </ItemGroup><ItemGroup>
  36.      <InternalsVisibleTo Include="MyTestProject" />
  37. </ItemGroup>  {
  38. <ItemGroup>
  39.      <InternalsVisibleTo Include="MyTestProject" />
  40. </ItemGroup><ItemGroup>
  41.      <InternalsVisibleTo Include="MyTestProject" />
  42. </ItemGroup><ItemGroup>
  43.      <InternalsVisibleTo Include="MyTestProject" />
  44. </ItemGroup> builder.UseEnvironment(Environments.Production);
  45. <ItemGroup>
  46.      <InternalsVisibleTo Include="MyTestProject" />
  47. </ItemGroup><ItemGroup>
  48.      <InternalsVisibleTo Include="MyTestProject" />
  49. </ItemGroup>  }).CreateClient(new WebApplicationFactoryClientOptions() { BaseAddress = new Uri("http://localhost:9000") });
  50. <ItemGroup>
  51.      <InternalsVisibleTo Include="MyTestProject" />
  52. </ItemGroup><ItemGroup>
  53.      <InternalsVisibleTo Include="MyTestProject" />
  54. </ItemGroup>  var token = TokenHelper.GetToken("username", "password");
  55. <ItemGroup>
  56.      <InternalsVisibleTo Include="MyTestProject" />
  57. </ItemGroup><ItemGroup>
  58.      <InternalsVisibleTo Include="MyTestProject" />
  59. </ItemGroup>  _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
  60. <ItemGroup>
  61.      <InternalsVisibleTo Include="MyTestProject" />
  62. </ItemGroup><ItemGroup>
  63.      <InternalsVisibleTo Include="MyTestProject" />
  64. </ItemGroup>  // Act
  65. <ItemGroup>
  66.      <InternalsVisibleTo Include="MyTestProject" />
  67. </ItemGroup>   }
  68. <ItemGroup>
  69.      <InternalsVisibleTo Include="MyTestProject" />
  70. </ItemGroup>   [Fact]
  71. <ItemGroup>
  72.      <InternalsVisibleTo Include="MyTestProject" />
  73. </ItemGroup>   public async Task V1Legacy_GetDeviceInfoes()
  74. <ItemGroup>
  75.      <InternalsVisibleTo Include="MyTestProject" />
  76. </ItemGroup>   {
  77. <ItemGroup>
  78.      <InternalsVisibleTo Include="MyTestProject" />
  79. </ItemGroup><ItemGroup>
  80.      <InternalsVisibleTo Include="MyTestProject" />
  81. </ItemGroup>  string url = "url1";
  82. <ItemGroup>
  83.      <InternalsVisibleTo Include="MyTestProject" />
  84. </ItemGroup><ItemGroup>
  85.      <InternalsVisibleTo Include="MyTestProject" />
  86. </ItemGroup>  // Arrange
  87. <ItemGroup>
  88.      <InternalsVisibleTo Include="MyTestProject" />
  89. </ItemGroup><ItemGroup>
  90.      <InternalsVisibleTo Include="MyTestProject" />
  91. </ItemGroup>  testOutputHelper.WriteLine($"Testing:{url}");
  92. <ItemGroup>
  93.      <InternalsVisibleTo Include="MyTestProject" />
  94. </ItemGroup><ItemGroup>
  95.      <InternalsVisibleTo Include="MyTestProject" />
  96. </ItemGroup>  var response = await _client.GetAsync(url);
  97. <ItemGroup>
  98.      <InternalsVisibleTo Include="MyTestProject" />
  99. </ItemGroup><ItemGroup>
  100.      <InternalsVisibleTo Include="MyTestProject" />
  101. </ItemGroup>  // Assert
  102. <ItemGroup>
  103.      <InternalsVisibleTo Include="MyTestProject" />
  104. </ItemGroup><ItemGroup>
  105.      <InternalsVisibleTo Include="MyTestProject" />
  106. </ItemGroup>  response.EnsureSuccessStatusCode(); // Status Code 200-299
  107. <ItemGroup>
  108.      <InternalsVisibleTo Include="MyTestProject" />
  109. </ItemGroup><ItemGroup>
  110.      <InternalsVisibleTo Include="MyTestProject" />
  111. </ItemGroup>  var result = await response.Content.ReadAsStringAsync();
  112. <ItemGroup>
  113.      <InternalsVisibleTo Include="MyTestProject" />
  114. </ItemGroup><ItemGroup>
  115.      <InternalsVisibleTo Include="MyTestProject" />
  116. </ItemGroup>  var target = JsonSerializer.Deserialize<DeviceInfo>(result, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
  117. <ItemGroup>
  118.      <InternalsVisibleTo Include="MyTestProject" />
  119. </ItemGroup><ItemGroup>
  120.      <InternalsVisibleTo Include="MyTestProject" />
  121. </ItemGroup>  Assert.NotNull(target);
  122. <ItemGroup>
  123.      <InternalsVisibleTo Include="MyTestProject" />
  124. </ItemGroup>   }
  125. <ItemGroup>
  126.      <InternalsVisibleTo Include="MyTestProject" />
  127. </ItemGroup>   [Fact]
  128. <ItemGroup>
  129.      <InternalsVisibleTo Include="MyTestProject" />
  130. </ItemGroup>   public async Task V1Legacy_GetCurrent()
  131. <ItemGroup>
  132.      <InternalsVisibleTo Include="MyTestProject" />
  133. </ItemGroup>   {
  134. <ItemGroup>
  135.      <InternalsVisibleTo Include="MyTestProject" />
  136. </ItemGroup><ItemGroup>
  137.      <InternalsVisibleTo Include="MyTestProject" />
  138. </ItemGroup>  var url = "url2";
  139. <ItemGroup>
  140.      <InternalsVisibleTo Include="MyTestProject" />
  141. </ItemGroup><ItemGroup>
  142.      <InternalsVisibleTo Include="MyTestProject" />
  143. </ItemGroup>  // Arrange
  144. <ItemGroup>
  145.      <InternalsVisibleTo Include="MyTestProject" />
  146. </ItemGroup><ItemGroup>
  147.      <InternalsVisibleTo Include="MyTestProject" />
  148. </ItemGroup>  testOutputHelper.WriteLine($"Testing:{url}");
  149. <ItemGroup>
  150.      <InternalsVisibleTo Include="MyTestProject" />
  151. </ItemGroup><ItemGroup>
  152.      <InternalsVisibleTo Include="MyTestProject" />
  153. </ItemGroup>  var response = await _client.GetAsync(url);
  154. <ItemGroup>
  155.      <InternalsVisibleTo Include="MyTestProject" />
  156. </ItemGroup><ItemGroup>
  157.      <InternalsVisibleTo Include="MyTestProject" />
  158. </ItemGroup>  // Assert
  159. <ItemGroup>
  160.      <InternalsVisibleTo Include="MyTestProject" />
  161. </ItemGroup><ItemGroup>
  162.      <InternalsVisibleTo Include="MyTestProject" />
  163. </ItemGroup>  response.EnsureSuccessStatusCode(); // Status Code 200-299
  164. <ItemGroup>
  165.      <InternalsVisibleTo Include="MyTestProject" />
  166. </ItemGroup><ItemGroup>
  167.      <InternalsVisibleTo Include="MyTestProject" />
  168. </ItemGroup>  var result = await response.Content.ReadAsStringAsync();
  169. <ItemGroup>
  170.      <InternalsVisibleTo Include="MyTestProject" />
  171. </ItemGroup><ItemGroup>
  172.      <InternalsVisibleTo Include="MyTestProject" />
  173. </ItemGroup>  var target = JsonSerializer.Deserialize<DeviceDataDto>(result, new JsonSerializerOptions {PropertyNameCaseInsensitive = true });
  174. <ItemGroup>
  175.      <InternalsVisibleTo Include="MyTestProject" />
  176. </ItemGroup><ItemGroup>
  177.      <InternalsVisibleTo Include="MyTestProject" />
  178. </ItemGroup>  Assert.NotNull(target);
  179. <ItemGroup>
  180.      <InternalsVisibleTo Include="MyTestProject" />
  181. </ItemGroup>   }
  182. <ItemGroup>
  183.      <InternalsVisibleTo Include="MyTestProject" />
  184. </ItemGroup>   [Theory]
  185. <ItemGroup>
  186.      <InternalsVisibleTo Include="MyTestProject" />
  187. </ItemGroup>   [InlineData("url3")]
  188. <ItemGroup>
  189.      <InternalsVisibleTo Include="MyTestProject" />
  190. </ItemGroup>   [InlineData("url4")]
  191. <ItemGroup>
  192.      <InternalsVisibleTo Include="MyTestProject" />
  193. </ItemGroup>   public async Task V1Legacy_CheckUrlExist(string url)
  194. <ItemGroup>
  195.      <InternalsVisibleTo Include="MyTestProject" />
  196. </ItemGroup>   {
  197. <ItemGroup>
  198.      <InternalsVisibleTo Include="MyTestProject" />
  199. </ItemGroup><ItemGroup>
  200.      <InternalsVisibleTo Include="MyTestProject" />
  201. </ItemGroup>  // Arrange
  202. <ItemGroup>
  203.      <InternalsVisibleTo Include="MyTestProject" />
  204. </ItemGroup><ItemGroup>
  205.      <InternalsVisibleTo Include="MyTestProject" />
  206. </ItemGroup>  testOutputHelper.WriteLine($"Testing:{url}");
  207. <ItemGroup>
  208.      <InternalsVisibleTo Include="MyTestProject" />
  209. </ItemGroup><ItemGroup>
  210.      <InternalsVisibleTo Include="MyTestProject" />
  211. </ItemGroup>  var request = new HttpRequestMessage(HttpMethod.Head, url);
  212. <ItemGroup>
  213.      <InternalsVisibleTo Include="MyTestProject" />
  214. </ItemGroup><ItemGroup>
  215.      <InternalsVisibleTo Include="MyTestProject" />
  216. </ItemGroup>  var response = await _client.SendAsync(request);
  217. <ItemGroup>
  218.      <InternalsVisibleTo Include="MyTestProject" />
  219. </ItemGroup><ItemGroup>
  220.      <InternalsVisibleTo Include="MyTestProject" />
  221. </ItemGroup>  // Assert
  222. <ItemGroup>
  223.      <InternalsVisibleTo Include="MyTestProject" />
  224. </ItemGroup><ItemGroup>
  225.      <InternalsVisibleTo Include="MyTestProject" />
  226. </ItemGroup>  Assert.NotEqual(404, (int)response.StatusCode);
  227. <ItemGroup>
  228.      <InternalsVisibleTo Include="MyTestProject" />
  229. </ItemGroup>   }
  230.     }
复制代码
在这个测试中,使用 IClassFixture 进行集成测试,确保同一个类之内的代码共享同一个资源,不同测试方法串行执行。
TIPS: 这里我使用 HEAD 请求来探查给定地址是否存在,ASP. NET CORE 会默认拒绝这个请求(返回406),但是不会提示 404 的错误。
现在的运行时间是这样的:

单类优化

首先研究为什么这个程序花费了如此多的时间执行测试,XUnit 在进行不同 Fact 的测试时,会生成不同的对象,我们已经通过实现 IClassFixture 共享了必要的数据吗?
并没有,XUnit 只是注入了 WebApplicationFactory,而我们在构造函数中执行了很多费时间的操作,包括构造 HttpClient ,获取 token 等。由于获取 token 的函数需要调用外部服务花费了很长的时间,我们可以尝试注入 HttpClient 进行优化。
请注意:大多数情况注入 HttpClient 不是一个好主意,更推荐利用 WebApplicationFactory 对每个测试动态生成 HttpClient 以保证 HttpClient 是初始干净的状态。
我们加入一个新的类:
  1.     public class SharedHttpClientFixture : IDisposable
  2.     {
  3. <ItemGroup>
  4.      <InternalsVisibleTo Include="MyTestProject" />
  5. </ItemGroup>   public HttpClient Client { get; init; }
  6. <ItemGroup>
  7.      <InternalsVisibleTo Include="MyTestProject" />
  8. </ItemGroup>   public SharedHttpClientFixture()
  9. <ItemGroup>
  10.      <InternalsVisibleTo Include="MyTestProject" />
  11. </ItemGroup>   {
  12. <ItemGroup>
  13.      <InternalsVisibleTo Include="MyTestProject" />
  14. </ItemGroup><ItemGroup>
  15.      <InternalsVisibleTo Include="MyTestProject" />
  16. </ItemGroup>  WebApplicationFactory<YourAssemblyName.Program> factory = new();
  17. <ItemGroup>
  18.      <InternalsVisibleTo Include="MyTestProject" />
  19. </ItemGroup><ItemGroup>
  20.      <InternalsVisibleTo Include="MyTestProject" />
  21. </ItemGroup>  
  22. <ItemGroup>
  23.      <InternalsVisibleTo Include="MyTestProject" />
  24. </ItemGroup><ItemGroup>
  25.      <InternalsVisibleTo Include="MyTestProject" />
  26. </ItemGroup><ItemGroup>
  27.      <InternalsVisibleTo Include="MyTestProject" />
  28. </ItemGroup> Client = factory.WithWebHostBuilder(builder =>
  29. <ItemGroup>
  30.      <InternalsVisibleTo Include="MyTestProject" />
  31. </ItemGroup><ItemGroup>
  32.      <InternalsVisibleTo Include="MyTestProject" />
  33. </ItemGroup><ItemGroup>
  34.      <InternalsVisibleTo Include="MyTestProject" />
  35. </ItemGroup><ItemGroup>
  36.      <InternalsVisibleTo Include="MyTestProject" />
  37. </ItemGroup>    {
  38. <ItemGroup>
  39.      <InternalsVisibleTo Include="MyTestProject" />
  40. </ItemGroup><ItemGroup>
  41.      <InternalsVisibleTo Include="MyTestProject" />
  42. </ItemGroup><ItemGroup>
  43.      <InternalsVisibleTo Include="MyTestProject" />
  44. </ItemGroup><ItemGroup>
  45.      <InternalsVisibleTo Include="MyTestProject" />
  46. </ItemGroup><ItemGroup>
  47.      <InternalsVisibleTo Include="MyTestProject" />
  48. </ItemGroup>   builder.UseEnvironment(Environments.Production);
  49. <ItemGroup>
  50.      <InternalsVisibleTo Include="MyTestProject" />
  51. </ItemGroup><ItemGroup>
  52.      <InternalsVisibleTo Include="MyTestProject" />
  53. </ItemGroup><ItemGroup>
  54.      <InternalsVisibleTo Include="MyTestProject" />
  55. </ItemGroup><ItemGroup>
  56.      <InternalsVisibleTo Include="MyTestProject" />
  57. </ItemGroup>    }).CreateClient(new WebApplicationFactoryClientOptions() { BaseAddress = new Uri("http://localhost:9000") });
  58. <ItemGroup>
  59.      <InternalsVisibleTo Include="MyTestProject" />
  60. </ItemGroup><ItemGroup>
  61.      <InternalsVisibleTo Include="MyTestProject" />
  62. </ItemGroup>  var token = TokenHelper.GetToken("username", "password");
  63. <ItemGroup>
  64.      <InternalsVisibleTo Include="MyTestProject" />
  65. </ItemGroup><ItemGroup>
  66.      <InternalsVisibleTo Include="MyTestProject" />
  67. </ItemGroup>  Client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
  68. <ItemGroup>
  69.      <InternalsVisibleTo Include="MyTestProject" />
  70. </ItemGroup><ItemGroup>
  71.      <InternalsVisibleTo Include="MyTestProject" />
  72. </ItemGroup>  // Act
  73. <ItemGroup>
  74.      <InternalsVisibleTo Include="MyTestProject" />
  75. </ItemGroup>   }
  76. <ItemGroup>
  77.      <InternalsVisibleTo Include="MyTestProject" />
  78. </ItemGroup>   public void Dispose()
  79. <ItemGroup>
  80.      <InternalsVisibleTo Include="MyTestProject" />
  81. </ItemGroup>   {
  82. <ItemGroup>
  83.      <InternalsVisibleTo Include="MyTestProject" />
  84. </ItemGroup><ItemGroup>
  85.      <InternalsVisibleTo Include="MyTestProject" />
  86. </ItemGroup>  //throw new NotImplementedException();
  87. <ItemGroup>
  88.      <InternalsVisibleTo Include="MyTestProject" />
  89. </ItemGroup>   }
  90.     }
复制代码
并修改测试类的签名:
  1.     public class ProgramTests : IClassFixture<SharedHttpClientFixture>
  2.     {
  3. <ItemGroup>
  4.      <InternalsVisibleTo Include="MyTestProject" />
  5. </ItemGroup>   private readonly ITestOutputHelper testOutputHelper;
  6. <ItemGroup>
  7.      <InternalsVisibleTo Include="MyTestProject" />
  8. </ItemGroup>   private readonly HttpClient _client;
  9. <ItemGroup>
  10.      <InternalsVisibleTo Include="MyTestProject" />
  11. </ItemGroup>   public ProgramTests(SharedHttpClientFixture httpClientFixture, ITestOutputHelper testOutputHelper)
  12. <ItemGroup>
  13.      <InternalsVisibleTo Include="MyTestProject" />
  14. </ItemGroup>   {
  15. <ItemGroup>
  16.      <InternalsVisibleTo Include="MyTestProject" />
  17. </ItemGroup><ItemGroup>
  18.      <InternalsVisibleTo Include="MyTestProject" />
  19. </ItemGroup>  _client = httpClientFixture.Client;
  20. <ItemGroup>
  21.      <InternalsVisibleTo Include="MyTestProject" />
  22. </ItemGroup><ItemGroup>
  23.      <InternalsVisibleTo Include="MyTestProject" />
  24. </ItemGroup>  this.testOutputHelper = testOutputHelper;
  25. <ItemGroup>
  26.      <InternalsVisibleTo Include="MyTestProject" />
  27. </ItemGroup>   }
复制代码
改完之后,速度提升效果还是非常显著的:

跨类串行

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

跨类并行(数据不共享)

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

跨类并行(数据共享)

由于任务并行无法得知其他任务的工作状态,这个时候数据共享可能会引入很多线程问题(竞争、死锁等),因此不太建议在这种情况下进行共享,我最终也是使用的并行不共享的方式实现。如果我们非得这么用,也不是不行,我们需要小心处理线程同步问题,以互斥锁为例:
  1. public class SharedHttpClientFixture : IDisposable
  2.     {
  3. <ItemGroup>
  4.      <InternalsVisibleTo Include="MyTestProject" />
  5. </ItemGroup>   private static HttpClient _httpClient;
  6. <ItemGroup>
  7.      <InternalsVisibleTo Include="MyTestProject" />
  8. </ItemGroup>   public HttpClient Client => GetClient();
  9. <ItemGroup>
  10.      <InternalsVisibleTo Include="MyTestProject" />
  11. </ItemGroup>   private HttpClient GetClient()
  12. <ItemGroup>
  13.      <InternalsVisibleTo Include="MyTestProject" />
  14. </ItemGroup>   {
  15. <ItemGroup>
  16.      <InternalsVisibleTo Include="MyTestProject" />
  17. </ItemGroup><ItemGroup>
  18.      <InternalsVisibleTo Include="MyTestProject" />
  19. </ItemGroup>  if (_httpClient == null) Init();
  20. <ItemGroup>
  21.      <InternalsVisibleTo Include="MyTestProject" />
  22. </ItemGroup><ItemGroup>
  23.      <InternalsVisibleTo Include="MyTestProject" />
  24. </ItemGroup>  return _httpClient;
  25. <ItemGroup>
  26.      <InternalsVisibleTo Include="MyTestProject" />
  27. </ItemGroup>   }
  28. <ItemGroup>
  29.      <InternalsVisibleTo Include="MyTestProject" />
  30. </ItemGroup>   public static Mutex count = new();
  31. <ItemGroup>
  32.      <InternalsVisibleTo Include="MyTestProject" />
  33. </ItemGroup>   public SharedHttpClientFixture()
  34. <ItemGroup>
  35.      <InternalsVisibleTo Include="MyTestProject" />
  36. </ItemGroup>   {
  37. <ItemGroup>
  38.      <InternalsVisibleTo Include="MyTestProject" />
  39. </ItemGroup>   }
  40. <ItemGroup>
  41.      <InternalsVisibleTo Include="MyTestProject" />
  42. </ItemGroup>   private void Init()
  43. <ItemGroup>
  44.      <InternalsVisibleTo Include="MyTestProject" />
  45. </ItemGroup>   {
  46. <ItemGroup>
  47.      <InternalsVisibleTo Include="MyTestProject" />
  48. </ItemGroup><ItemGroup>
  49.      <InternalsVisibleTo Include="MyTestProject" />
  50. </ItemGroup>  count.WaitOne();
  51. <ItemGroup>
  52.      <InternalsVisibleTo Include="MyTestProject" />
  53. </ItemGroup><ItemGroup>
  54.      <InternalsVisibleTo Include="MyTestProject" />
  55. </ItemGroup>  if(_httpClient == null)
  56. <ItemGroup>
  57.      <InternalsVisibleTo Include="MyTestProject" />
  58. </ItemGroup><ItemGroup>
  59.      <InternalsVisibleTo Include="MyTestProject" />
  60. </ItemGroup>  {
  61.                                 ...
  62. <ItemGroup>
  63.      <InternalsVisibleTo Include="MyTestProject" />
  64. </ItemGroup><ItemGroup>
  65.      <InternalsVisibleTo Include="MyTestProject" />
  66. </ItemGroup>  }
  67. <ItemGroup>
  68.      <InternalsVisibleTo Include="MyTestProject" />
  69. </ItemGroup><ItemGroup>
  70.      <InternalsVisibleTo Include="MyTestProject" />
  71. </ItemGroup>  count.ReleaseMutex();
  72. <ItemGroup>
  73.      <InternalsVisibleTo Include="MyTestProject" />
  74. </ItemGroup>   }
  75. <ItemGroup>
  76.      <InternalsVisibleTo Include="MyTestProject" />
  77. </ItemGroup>   public void Dispose()
  78. <ItemGroup>
  79.      <InternalsVisibleTo Include="MyTestProject" />
  80. </ItemGroup>   {
  81. <ItemGroup>
  82.      <InternalsVisibleTo Include="MyTestProject" />
  83. </ItemGroup><ItemGroup>
  84.      <InternalsVisibleTo Include="MyTestProject" />
  85. </ItemGroup>  //throw new NotImplementedException();
  86. <ItemGroup>
  87.      <InternalsVisibleTo Include="MyTestProject" />
  88. </ItemGroup>   }
  89. }
复制代码
这样多个类型使用静态变量实现了共享,并利用互斥锁保证初始化只执行一次。

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

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

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

默认情况下 Program 是其他项目不可见的,这样会导致 WebApplicationFactory 提示错误。.NET 6 开始引入了 minimal API,我的项目是升级而来,并没有使用到这个东西,所以 Program 类是对外可见的。
  1.     public class Program
  2.     {
  3. <ItemGroup>
  4.      <InternalsVisibleTo Include="MyTestProject" />
  5. </ItemGroup>   static void Main(string[] args)
  6. <ItemGroup>
  7.      <InternalsVisibleTo Include="MyTestProject" />
  8. </ItemGroup>   {
  9. <ItemGroup>
  10.      <InternalsVisibleTo Include="MyTestProject" />
  11. </ItemGroup><ItemGroup>
  12.      <InternalsVisibleTo Include="MyTestProject" />
  13. </ItemGroup>  var builder = WebApplication.CreateBuilder(args);
  14. <ItemGroup>
  15.      <InternalsVisibleTo Include="MyTestProject" />
  16. </ItemGroup><ItemGroup>
  17.      <InternalsVisibleTo Include="MyTestProject" />
  18. </ItemGroup>  builder.WebHost.UseUrls("http://*:9000");
  19. <ItemGroup>
  20.      <InternalsVisibleTo Include="MyTestProject" />
  21. </ItemGroup><ItemGroup>
  22.      <InternalsVisibleTo Include="MyTestProject" />
  23. </ItemGroup>  ConfigureServices(builder.Services, builder.Configuration);
  24. <ItemGroup>
  25.      <InternalsVisibleTo Include="MyTestProject" />
  26. </ItemGroup><ItemGroup>
  27.      <InternalsVisibleTo Include="MyTestProject" />
  28. </ItemGroup>  var app = builder.Build();
  29. <ItemGroup>
  30.      <InternalsVisibleTo Include="MyTestProject" />
  31. </ItemGroup><ItemGroup>
  32.      <InternalsVisibleTo Include="MyTestProject" />
  33. </ItemGroup>  Configure(app);
  34. <ItemGroup>
  35.      <InternalsVisibleTo Include="MyTestProject" />
  36. </ItemGroup><ItemGroup>
  37.      <InternalsVisibleTo Include="MyTestProject" />
  38. </ItemGroup>  app.Run();
  39. <ItemGroup>
  40.      <InternalsVisibleTo Include="MyTestProject" />
  41. </ItemGroup>   }
  42.     }
复制代码
如果使用 Minimal API,那么你需要在项目文件中对测试项目公开可见性。
  1. <ItemGroup>
  2.      <InternalsVisibleTo Include="MyTestProject" />
  3. </ItemGroup>
复制代码
或者在 Program.cs 的最后加上一行。
  1. var builder = WebApplication.CreateBuilder(args);
  2. // ... Configure services, routes, etc.
  3. app.Run();
  4. + 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)

来源:https://www.cnblogs.com/podolski/archive/2023/05/10/17388602.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具