冠军

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

.NET Aspire Apps 集成测试

原文:https://fiodar.substack.com/p/integration-testing-dotnet-aspire-apps

对于软件开发来说,拥有自动化的覆盖测试非常重要。尽管手工测试在有些场合存在其价值,自动化测试对于支持我们验证应用的处理逻辑比任何手工测试更加高效。

自动化测试还支持我们检查在某一个地方的变更不会意外地破坏其它的任何部分,这是另一个手工测试复杂应用几乎不可能做到的领域,尽管我们对任何新的功能实现添加了测试 (甚至使用了 TDD 模式,在实现功能之前就增加了测试用例),但我们仍然会拥有之前添加的所有先前功能的所有先前添加的测试。

集成测试是一类特殊的自动化测试,它允许我们验证应用程序或模块是否按预期与其他组件一起工作。这些类型的测试可能使用真实的网络、真实的 HTTP 请求、真实的数据库连接等。

传统上,集成测试相当难以应用于编排的分布式应用程序。这是因为此类应用程序有许多必须相互协调的移动组件。这些组件的计算成本通常很高;因此,集成测试通常是为了模拟有限数量的服务间交互。

但是,.NET Aspire 非常巧妙地解决了这个问题。Microsoft 的设计人员提前考虑了集成测试。因此,针对它运行集成测试几乎与运行低级组件/单元测试一样简单。

此外,Aspire 甚至附带了一个集成测试项目的模板,因此我们不必在每次需要将测试添加到新的分布式应用程序时都搜索 Internet。这就是我们今天要讨论的内容。

在 Aspire 中设置集成测试

最为简单的设置一个测试项目的途径是,在通过 Visual Studio 中创建 Aspire Starter Project 应用的时候,直接选择上 Create test project 选项

如果您是 dotnet CLI 用户,则可以执行以下命令来创建具有所有依赖项的新 Aspire 测试项目:

dotnet new aspire-xunit

默认情况下,Aspire 测试项目模板使用 xUnit。但是,我们将讨论的所有概念也可以在 NUnit 和 MSTest 中实现。Aspire 测试库与框架无关,可与其中任何一个一起使用。

我们甚至不必使用默认测试模板。我们可以使用任何测试框架在 .NET 中创建标准测试项目,然后安装特定于 Aspire 集成测试的依赖项。

安装必须的依赖

此 GitHub 存储库中,我们有一个从 Aspire 入门项目模板创建的 Aspire 项目示例。它添加了一个名为 AspireApp.Tests 的测试项目。如果我们打开 AspireApp.Tests.csproj 文件,我们将发现其中引用了 Aspire.Hosting.Testing NuGet 包。这是包含 Aspire 集成测试所需的所有组件的软件包。

项目引用的其余包与测试框架相关,并不特定于 Aspire。如果我们使用不同的测试框架,我们会看到引用不同的包。

因此,我们拥有开始编写测试所需的所有依赖项。当我们创建新的 Aspire 入门项目时,最好从默认提供的测试方法开始。它很简单,很容易让我们了解,但它也演示了 Aspire 测试库的核心功能。

研究 Aspire 的测试方法

如果我们开发 AspireApp.Test 项目中的 WebTests.cs 代码文件,我们将会看到如下的测试方法:

[Fact]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
    // Arrange
    var appHost = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.AspireApp_AppHost>();
    await using var app = await appHost.BuildAsync();
    await app.StartAsync();

    // Act
    var httpClient = app.CreateHttpClient("webfrontend");
    var response = await httpClient.GetAsync("/");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

这是 arrange-act-assert 测试模式的一个相当标准的示例。此模式规定每个测试方法都包含以下不同的部分:

  • Arrange 安排:我们设置了运行测试所需的所有服务。
  • Act 行动:我们执行我们正在测试的操作。
  • Assert 断言:我们将验证此操作的结果。

在此示例中(通常在 C# 中应用),我们用注释标记每个部分,以使测试方法尽可能易于理解。

让我们逐行介绍此方法以了解它在做什么。我们采取的第一个操作如下:

var appHost = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.AspireApp_AppHost>();

这段代码使用 Aspire.Hosting.Testing 库中的 DistributedApplicationTestingBuilder 类型。我们调用静态 CreateAsync<T>() 方法来初始化分布式和编排的 Aspire 应用程序。

我们在调用此方法时指定的类型是 Projects.AspireApp_AppHost,它是我们 Aspire Host 项目的自动生成的表示形式。从本质上讲,我们正在创建整个 Aspire 应用程序的测试版本。

我们不需要太深入研究 test host 和 normal host 之间的区别,但了解测试 host 要轻量级得多是有帮助的。但不要被它很轻的事实所欺骗。我们仍然能够测试 Aspire 应用程序的所有核心功能,以及其编排的服务如何相互操作。测试版本的 Host 仍然使用真实网络和其他真实依赖项。

随后的 2 行代码,构建 Host 并启动它。

await using var app = await appHost.BuildAsync();
await app.StartAsync();

请注意,我们将 app 对象(表示整个 Aspire 应用程序)包装在 using 范围内。这是因为它是一个一次性对象,依赖于各种非托管资源,例如网络连接。一旦代码退出 using 范围,它就会完全释放这些资源。

随后的代码是这样的:

var httpClient = app.CreateHttpClient("webfrontend");

这段代码从 Aspire 测试主机 Host 中来创建 HttpClient 类的实例。因为我们从 Aspire 主机 Host 中创建它,所以我们使用 Aspire 服务发现来设置其基本 URL。在此示例中,我们在 webfrontend 的名称下注册一个 Blazor 应用。我们的 HTTP 客户端将使用该应用程序的基本 URL 进行配置。

接下来,我们向应用程序的基址发送一个 GET HTTP 请求,该地址用正斜杠表示:

var response = await httpClient.GetAsync("/");

最后,我们验证我们收到的是 HTTP OK (200) 的响应码。

Assert.Equal(HttpStatusCode.OK, response.StatusCode);

这是我们对使用 xUnit 实现的基本示例测试方法的概述。现在让我们看看如何使用替代测试框架来实现相同的方法。

使用其它的测试框架

如果我们希望使用 NUnit 来替换掉 xUnit 测试框架,我们的测试方法将会变成这样。

[Test]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
    // Arrange
    var appHost = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.AspireApp_AppHost>();
    await using var app = await appHost.BuildAsync();
    await app.StartAsync();

    // Act
    var httpClient = app.CreateHttpClient("webfrontend");
    var response = await httpClient.GetAsync("/");

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}

在这种情况下,xUnit 和 NUnit 之间的主要区别在于使用 [Test] 而不是 [Fact],使用 Assert.AreEqual() 而不是 Assert.Equal()。否则,测试的结构和逻辑将保持不变。

如果我们首选的测试框架是 MSTest,则方法将如下所示:

[TestMethod]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
    // Arrange
    var appHost = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.AspireApp_AppHost>();
    await using var app = await appHost.BuildAsync();
    await app.StartAsync();

    // Act
    var httpClient = app.CreateHttpClient("webfrontend");
    var response = await httpClient.GetAsync("/");

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}

在 MSTest 中,属性 [TestMethod] 用于指示测试方法。断言是使用 Assert.AreEqual() 完成的,类似于 NUnit。测试的其余部分与原始测试的结构和逻辑保持一致。

扩展测试功能

在前面我们研究的测试方法中,您可能已经注意到的一点是它非常简单。事实上,它是如此简单,以至于它的用处可能非常有限。我们所做的只是向应用程序的主页发送一个非常基本的 HTTP 请求,并验证我们是否收到了 200 响应代码。

改进测试的一种方法是查看响应中返回的内容。这适用于具有用户界面的浏览器内应用程序和无头 API 应用程序。在 .NET 中有很多方法可以做到这一点,每种方法都有其优点和缺点。

我们将使用 AngleSharp NuGet 包,它允许我们解析网页上的 HTML 并轻松搜索其内容。从测试项目中引用此 NuGet 包后,我们可以创建以下帮助程序类,其中包含允许我们读取 HTTP 响应对象并分析其内容的方法:

AngleSharp 是一个 .NET 库,使您能够解析基于尖括号的超文本,如 HTML、SVG 和 MathML。该库还支持未经验证的 XML。AngleSharp 的一个重要方面是 CSS 也可以被解析。包含的解析器基于官方 W3C 规范构建。这将生成给定源代码的完美可移植 HTML5 DOM 表示,并确保与常青浏览器中的结果兼容。此外,标准的 DOM 功能(如 querySelector 或 querySelectorAll)也适用于树遍历。

using AngleSharp;
using AngleSharp.Html.Dom;
using AngleSharp.Io;
using System.Net.Http.Headers;

namespace AspireApp.Tests.Helpers;

public class HtmlHelpers
{
    public static async Task<IHtmlDocument> GetDocumentAsync(HttpResponseMessage response)
    {
        var content = await response.Content.ReadAsStringAsync();
        var document = await BrowsingContext.New()
            .OpenAsync(ResponseFactory, CancellationToken.None);
        return (IHtmlDocument)document;

        void ResponseFactory(VirtualResponse htmlResponse)
        {
            htmlResponse
                .Address(response.RequestMessage.RequestUri)
                .Status(response.StatusCode);

                MapHeaders(response.Headers);
                MapHeaders(response.Content.Headers);

                htmlResponse.Content(content);

                void MapHeaders(HttpHeaders headers)
                {
                    foreach (var header in headers)
                    {
                        foreach (var value in header.Value)
                        {
                            htmlResponse.Header(header.Key, value);
                        }
                     }
                 }
         }
    }
}

该方法将一个 HttpResponseMessage 转化为一个 AngleSharp 的 IHtmlDocument 对象,以方便后继的处理。

在我们这个分布式示例应用中,Aspire 应用的前端是一个 Blazor 应用,通过访问独立的 REST API 应用来获得数据,填充到它的气象页面中。

我们开发一个测试用来来检查气象页面是否拥有我们所期望的内容

[Fact]
public async Task GetWeatherReturnsRightContent()
{
    // Arrange
    var appHost = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.AspireApp_AppHost>();
    await using var app = await appHost.BuildAsync();
    await app.StartAsync();

    // Act
    var httpClient = app.CreateHttpClient("webfrontend");

    var response = await httpClient.GetAsync("/weather");
    var responseBody = await HtmlHelpers.GetDocumentAsync(response);
    var descriptionElement = responseBody.QuerySelector("p");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    Assert.NotNull(descriptionElement);
    Assert.Equal(
        "This component demonstrates showing data loaded from a backend API service.",
    descriptionElement.InnerHtml);
}

在这个测试方法中,我们不是简单地检查页面是否存在并返回适当的响应代码。我们还通过将页面内容传递给我们之前创建的 helper 方法来读取页面的内容。然后我们搜索第一个段落元素 p 的 HTML 元素,并检查其内容是否为 This component demonstrates showing data loaded from a backend API service.

现在,我们的测试更加有用。

组件测试呢?

虽然集成测试是有益的,而且我们在分布式应用程序中可能需要它们,但我们也可以从测试单个组件中受益。这些类型的测试要快得多,因为它们不依赖于任何真正的网络。他们直接测试代码。

针对 Blazor 应用程序编写组件测试的最佳方法之一是使用 Bunit NuGet 包。这个包甚至得到了 Microsoft 的认可,并在其页面上被提及,尽管它是由开源社区制作的。

我们需要在应用程序中安装两个 NuGet 包:Bunit 和 Bunit.TestDoubles。现在,让我们看看如何使用这个包来测试这个页面:

bUnit is a testing library for Blazor Components. Its goal is to make it easy to write comprehensive, stable unit tests.

这是 Blazor 项目模板中的标准 Counter 页面。它有一个按钮,单击该按钮时,屏幕上的计数会增加。我们可以使用 Bunit 来测试此功能。以下是我们为它编写的测试:

public class CounterTests : BunitTestContext
{
    [Fact]
    public void CounterStartsAtZero()
    {
        // Arrange
        var cut = RenderComponent<Counter>();

        // Assert
        cut.Find("p")
            .MarkupMatches(
                "<p role=\"status\">Current count: 0</p>");
    }

    [Fact]
    public void ClickingButtonIncrementsCounter()
    {
        // Arrange
        var cut = RenderComponent<Counter>();

        // Act
        cut.Find("button").Click();

        // Assert
        cut.Find("p")
            .MarkupMatches(
                "<p role=\"status\">Current count: 1</p>");
    }
}

第一个测试方法 CounterStartsAtZero() 检查初始计数器值是否为零。它通过直接呈现 Counter 组件并检查相应的 HTML 是否包含 0 作为 count 值来实现这一点。

另一个测试方法 ClickingButtonIncrementsCounter() 检查在单击按钮时计数器是否递增。

总结

与其他等效技术相比,.NET Aspire 不仅允许开发人员相对容易地构建分布式编排应用程序,而且还使针对这些应用程序编写集成测试变得容易。

下次,我们将讨论保护 Aspire 应用程序。我们将讨论如何保护控制面板和托管应用程序,因此请关注此空间。

posted on   冠军  阅读(60)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2010-12-19 微软官方的 SharePoint 支持中心以及在线实验室
2010-12-19 转:SharePoint Learning Kit
点击右上角即可分享
微信分享提示