[.NET Blog] .NET Aspire 测试入门
https://devblogs.microsoft.com/dotnet/getting-started-with-testing-and-dotnet-aspire/
自动化测试是软件开发的重要一环。它可以帮助我们尽早确认软件中的缺陷和防止回归问题。在本文中,我们将探讨如何在 .NET Aspire 中开始测试,支持我们进行跨分布式应用的测试场景。
测试分布式应用
分布式应用存在本质上的复杂性。你需要确保各种组件,例如数据库、缓存等等可用并且工作在正确的状态。然后,你的应用可能存在众多的服务需要一起进行测试。.NET Aspire 是非常棒的工具来帮助我们定义应用程序的环境,将众多的服务和资源连接在一起,进而比较容易地启动我们的环境。
对于端到端的测试也是这样,或者对于集成测试,我们需要确保数据库在期望的测试状态,避免其他的测试影响我们的测试,并且确保应用运行在正确的配置之下,这也是 .NET Aspire 可以提供帮助的地方。
谢天谢地,我们拥有了 .NET Aspire 的 Aspire.Hosting.Testing NuGet Package,它可以在这些方面帮助我们,让我们看一下我们可以如何使用它来编写测试用例。
入门
在开始的时候,我们将创建一个新的 .NET Aspire Starter App 项目,这将会创建一个新的使用了 AppHost 的 .NET Application,以及 Service Defaults, 和一个 API backend 和 一个 Blazor Web 前端项目。
首先确保你已经安装了 .NET Aspire workload
dotnet workload update
dotnet workload install aspire
然后,使用 aspire-starter
模板来创建新的项目。
dotnet new aspire-starter --name AspireWithTesting
然后,我们需要添加测试项目了,我们可以从 3 个测试框架中选择你希望的那个:
- MSTest
- xUnit
- Nunit
例如,我们这里使用 MSTest。这可以通过使用 aspire-mstest
模板来完成。
dotnet new aspire-mstest --name AspireWithTesting.Tests
dotnet sln add AspireWithTesting.Tests
注意:在使用
aspire-starter
模板创建项目的时候,通过使用参数--test-framework MSTest
,你可以直接在新创建的项目中包含该测试项目。
在模板中已经引用了 Aspire-Hosting.Testing
NuGet 包,以及选择的测试框架,在这里是 MSTest。所以,最后需要做的事情就是将测试项目添加到 AppHost 项目中。
dotnet add AspireWithTesting.Tests reference AspireWithTesting.AppHost
编写测试
你会发现这里已经提供了一个初始的测试文件,在上面描述中创建的测试项目中的 IntegrationTest1.cs
。它提供了一个测试的示例,不过,我们还是从头编写一个,以便我们理解需要做的内容。创建一个新的名为 FrontEndTests.cs
的文件并试下下面的内容:
namespace AspireWithTesting.Tests;
[TestClass]
public class FrontEndTests
{
[TestMethod]
public async Task CanGetIndexPage()
{
var appHost =
await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AspireWithTesting_AppHost>();
appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddStandardResilienceHandler();
});
await using var app = await appHost.BuildAsync();
await app.StartAsync();
var resourceNotificationService =
app.Services.GetRequiredService<ResourceNotificationService>();
await resourceNotificationService
.WaitForResourceAsync("webfrontend", KnownResourceStates.Running)
.WaitAsync(TimeSpan.FromSeconds(30));
var httpClient = app.CreateHttpClient("webfrontend");
var response = await httpClient.GetAsync("/");
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
}
好极了,测试已经写好了,让我们执行一下它。
dotnet test
如果所有的组件都在计划中执行,我们将应该看到如下的输出:
Test summary: total: 1, failed: 0, succeeded: 1, skipped: 0, duration: 0.9s
理解测试
让我分解上面的代码,理解在测试中发生了什么。
var appHost =
await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AspireWithTesting_AppHost>();
appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddStandardResilienceHandler();
});
await using var app = await appHost.BuildAsync();
await app.StartAsync();
测试的第一部分是利用在 AppHost 项目中定义的所有资源、服务和它们的关系,然后启动它们,如同在 AppHost 项目中执行 dotnet run
一样。但是在测试项目中,我们可以控制一些额外的方面。例如,我们可以注入 StandardResilienceHandler
到 HttpClient
中,这样测试中可以可以用来与 AppHost 中的服务交互。在 AppHost 配置之后,我们构建整个应用,准备开始执行测试。
var resourceNotificationService =
app.Services.GetRequiredService<ResourceNotificationService>();
await resourceNotificationService
.WaitForResourceAsync("webfrontend", KnownResourceStates.Running)
.WaitAsync(TimeSpan.FromSeconds(30));
因为 AppHost 将要启动多个不同的资源和服务,我们需要在基于它们执行我们的测试之前,确保它们可用。在这之后,如果 Web 应用没有正常启动,而我们试图访问它,我们就会得到一个错误。ResourceNotificationService
是用来帮助我们等待某个服务进入特定状态的服务。在我们的这个示例中,我们将等待 Webfrontend
(这是配置在 AppHost 中的名称) 进入 Running
状态,我们使用 30s 的时间来等待。这个模式将需要对任何我们需要交互的服务使用,不管是直接还是间接。
var httpClient = app.CreateHttpClient("webfrontend");
var response = await httpClient.GetAsync("/");
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
最后,我们可以从启动之后的应用中申请一个 HttpClient
的实例,提供希望访问的服务名称。这里将使用与以后的应用程序实际场景一样的服务发现机制,所以,我们不用担心服务的 URL 或者端口号。我们可以向服务发出请求,在我们的这个示例中,我们访问了 Web 前端的根目录 /
,并检查我们是否得到一个 200 OK
的响应。来确认服务正确执行并如期望进行响应。
测试 API
测试 API 的方式非常类似于测试前端服务的方式,因为它返回的是数据,我们可以更进一步,可以根据返回的数据进行断言。
using System.Net.Http.Json;
namespace AspireWithTesting.Tests;
[TestClass]
public class ApiTests
{
[TestMethod]
public async Task CanGetWeatherForecast()
{
var appHost =
await DistributedApplicationTestingBuilder.CreateAsync<Projects.AspireWithTesting_AppHost>();
appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddStandardResilienceHandler();
});
await using var app = await appHost.BuildAsync();
await app.StartAsync();
var resourceNotificationService =
app.Services.GetRequiredService<ResourceNotificationService>();
await resourceNotificationService
.WaitForResourceAsync("apiservice", KnownResourceStates.Running)
.WaitAsync(TimeSpan.FromSeconds(30));
var httpClient = app.CreateHttpClient("apiservice");
var response = await httpClient.GetAsync("/weatherforecast");
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
var forecasts = await response.Content.ReadFromJsonAsync<IEnumerable<WeatherForecast>>();
Assert.IsNotNull(forecasts);
Assert.AreEqual(5, forecasts.Count());
}
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);
}
注意:由于这里涉及的
record
类型的WeatherForecast
是在 API 项目中私有定义的,我们我们需要在测试项目中重新定义它,以便反序列化收到的 JSON 数据。
一旦我们断言 API 端点返回了期望的 200 OK
响应,我们可以进而反序列化收到的 JSON 响应为 WeatherForecast
对象的集合,并基于这些数据进一步断言。在我们的示例中,我们的 API 返回的是随机生成的数据,所以我们仅仅断言返回的 record
的数量,不过,如果我们的数据是来自于数据库中,那么,断言就可以根据期望的数据进行。
总结
在本文中,我们介绍了如何开始在 .NET Aspire 中进行测试。它使得我们可以处理分布式应用的测试场景。我们也看到了如何针对前端应用和 API 服务开发测试用例,确保它们运行并如期望进行响应。
Reference: