Поделиться через


Напишите свой первый тест .NET.NET Aspire

В этой статье вы узнаете, как создать тестовый проект, написать тесты и запустить их для .NET.NET Aspire решений. Тесты в этой статье — это не модульные тесты, а скорее функциональные или интеграционные тесты. .NET .NET Aspire включает несколько вариантов шаблонов проектов тестирования , которые можно использовать для тестирования зависимостей .NET.NET Aspire ресурсов и их взаимодействия. Шаблоны проектов тестирования доступны для платформ тестирования MSTest, NUnit и xUnit и включают пример теста, который можно использовать в качестве отправной точки для тестов.

Тестовые шаблоны проектов .NET.NET Aspire зависят от пакета NuGet 📦Aspire.Hosting.Testing. Этот пакет предоставляет класс DistributedApplicationTestingBuilder, который используется для создания тестового узла для распределенного приложения. Построитель распределенного тестирования приложений использует класс DistributedApplication для создания и запуска узла приложения.

Создание тестового проекта

Самый простой способ создать тестовый проект .NET.NET Aspire — использовать шаблон тестового проекта. Если вы запускаете новый проект .NET.NET Aspire и хотите включить тестовые проекты, средство Visual Studio поддерживает этот параметр. Если вы добавляете тестовый проект в существующий проект .NET.NET Aspire, можно использовать команду dotnet new для создания тестового проекта:

dotnet new aspire-xunit
dotnet new aspire-mstest
dotnet new aspire-nunit

Дополнительные сведения см. в документации команды .NET CLI dotnet new.

Изучение тестового проекта

Данный пример тестового проекта был создан в составе шаблона начального приложения . Если вы не знакомы с ним, прочитайте краткое руководство: создание вашего первого проекта. .NET.NET Aspire. Тестовый проект .NET.NET Aspire принимает зависимость ссылки на проект от целевого узла приложения. Рассмотрим проект шаблона:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="Aspire.Hosting.Testing" Version="9.0.0" />
    <PackageReference Include="coverlet.collector" Version="6.0.4" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
    <PackageReference Include="xunit" Version="2.9.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\AspireApp.AppHost\AspireApp.AppHost.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="System.Net" />
    <Using Include="Microsoft.Extensions.DependencyInjection" />
    <Using Include="Aspire.Hosting.ApplicationModel" />
    <Using Include="Aspire.Hosting.Testing" />
    <Using Include="Xunit" />
  </ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <PropertyGroup>
    <EnableMSTestRunner>true</EnableMSTestRunner>
    <OutputType>Exe</OutputType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Aspire.Hosting.Testing" Version="9.0.0" />
    <PackageReference Include="MSTest" Version="3.7.2" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\AspireApp.AppHost\AspireApp.AppHost.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="System.Net" />
    <Using Include="Microsoft.Extensions.DependencyInjection" />
    <Using Include="Aspire.Hosting.ApplicationModel" />
    <Using Include="Aspire.Hosting.Testing" />
    <Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
  </ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="Aspire.Hosting.Testing" Version="9.0.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
    <PackageReference Include="NUnit" Version="4.3.2" />
    <PackageReference Include="NUnit.Analyzers" Version="4.6.0" />
    <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\AspireApp.AppHost\AspireApp.AppHost.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="System.Net" />
    <Using Include="Microsoft.Extensions.DependencyInjection" />
    <Using Include="Aspire.Hosting.ApplicationModel" />
    <Using Include="Aspire.Hosting.Testing" />
    <Using Include="NUnit.Framework" />
  </ItemGroup>

</Project>

Предыдущий файл проекта довольно стандартный. В составе PackageReference и 📦Aspireнаходится пакет NuGet.Hosting.Testing, который включает необходимые типы для написания тестов для проектов .NET.NET Aspire.

Проект теста шаблона включает класс IntegrationTest1 с одним тестом. Тест проверяет следующий сценарий:

  • Хост приложения успешно создан и запущен.
  • Ресурс webfrontend доступен и запущен.
  • К ресурсу webfrontend можно выполнить HTTP-запрос, который возвратит успешный ответ (HTTP 200 OK).

Рассмотрим следующий тестовый класс:

namespace AspireApp.Tests;

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

        appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
        {
            clientBuilder.AddStandardResilienceHandler();
        });

        // To output logs to the xUnit.net ITestOutputHelper, 
        // consider adding a package from https://www.nuget.org/packages?q=xunit+logging

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

        var resourceNotificationService = app.Services
            .GetRequiredService<ResourceNotificationService>();

        await app.StartAsync();

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

        await resourceNotificationService.WaitForResourceAsync(
            "webfrontend",
            KnownResourceStates.Running
            )
            .WaitAsync(TimeSpan.FromSeconds(30));

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

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

[TestClass]
public class IntegrationTest1
{
    [TestMethod]
    public async Task GetWebResourceRootReturnsOkStatusCode()
    {
        // Arrange
        var appHost = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.AspireApp_AppHost>();

        appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
        {
            clientBuilder.AddStandardResilienceHandler();
        });

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

        var resourceNotificationService = app.Services
            .GetRequiredService<ResourceNotificationService>();

        await app.StartAsync();

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

        await resourceNotificationService.WaitForResourceAsync(
                "webfrontend",
                KnownResourceStates.Running
            )
            .WaitAsync(TimeSpan.FromSeconds(30));
        
        var response = await httpClient.GetAsync("/");

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

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

        appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
        {
            clientBuilder.AddStandardResilienceHandler();
        });

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

        var resourceNotificationService = app.Services
            .GetRequiredService<ResourceNotificationService>();

        await app.StartAsync();

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

        await resourceNotificationService.WaitForResourceAsync(
                "webfrontend",
                KnownResourceStates.Running
            )
            .WaitAsync(TimeSpan.FromSeconds(30));
        
        var response = await httpClient.GetAsync("/");

        // Assert
        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
    }
}

Предыдущий код:

  • Использует api DistributedApplicationTestingBuilder.CreateAsync для асинхронного создания узла приложения.
  • appHost вызывает метод IDistributedApplicationTestingBuilder.BuildAsync(CancellationToken), который возвращает экземпляр DistributedApplication в качестве app.
    • app предоставляет своему поставщику услуг задачу получения экземпляра ResourceNotificationService.
    • app запускается асинхронно.
  • Для ресурса HttpClient создается webfrontend путем вызова app.CreateHttpClient.
  • resourceNotificationService используется для ожидания доступности и запуска ресурса webfrontend.
  • Делается простой HTTP-запрос GET к корню ресурса webfrontend.
  • Тест утверждает, что код состояния ответа OK.

Переменные среды тестирования ресурсов

Для дальнейшего тестирования ресурсов и их выраженных зависимостей в решении .NET.NET Aspire можно подтвердить правильность внедрения переменных среды. В следующем примере показано, как проверить, что ресурс webfrontend имеет переменную среды HTTPS, которая ссылается на ресурс apiservice:

using Aspire.Hosting;

namespace AspireApp.Tests;

public class EnvVarTests
{
    [Fact]
    public async Task WebResourceEnvVarsResolveToApiService()
    {
        // Arrange
        var appHost = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.AspireApp_AppHost>();

        var frontend = (IResourceWithEnvironment)appHost.Resources
            .Single(static r => r.Name == "webfrontend");

        // Act
        var envVars = await frontend.GetEnvironmentVariableValuesAsync(
            DistributedApplicationOperation.Publish);

        // Assert
        Assert.Contains(envVars, static (kvp) =>
        {
            var (key, value) = kvp;

            return key is "services__apiservice__https__0"
                && value is "{apiservice.bindings.https.url}";
        });
    }
}
using Aspire.Hosting;

namespace AspireApp.Tests;

[TestClass]
public class EnvVarTests
{
    [TestMethod]
    public async Task WebResourceEnvVarsResolveToApiService()
    {
        // Arrange
        var appHost = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.AspireApp_AppHost>();

        var frontend = (IResourceWithEnvironment)appHost.Resources
            .Single(static r => r.Name == "webfrontend");

        // Act
        var envVars = await frontend.GetEnvironmentVariableValuesAsync(
            DistributedApplicationOperation.Publish);

        // Assert
        CollectionAssert.Contains(envVars,
            new KeyValuePair<string, string>(
                key: "services__apiservice__https__0",
                value: "{apiservice.bindings.https.url}"));
    }
}
using Aspire.Hosting;

namespace AspireApp.Tests;

public class EnvVarTests
{
    [Test]
    public async Task WebResourceEnvVarsResolveToApiService()
    {
        // Arrange
        var appHost = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.AspireApp_AppHost>();

        var frontend = (IResourceWithEnvironment)appHost.Resources
            .Single(static r => r.Name == "webfrontend");

        // Act
        var envVars = await frontend.GetEnvironmentVariableValuesAsync(
            DistributedApplicationOperation.Publish);

        // Assert
        Assert.That(envVars, Does.Contain(
            new KeyValuePair<string, string>(
                key: "services__apiservice__https__0",
                value: "{apiservice.bindings.https.url}")));
    }
}

Предыдущий код:

  • Использует api DistributedApplicationTestingBuilder.CreateAsync для асинхронного создания узла приложения.
  • Экземпляр builder используется для извлечения экземпляра IResourceWithEnvironment, названного "webfrontend", из IDistributedApplicationTestingBuilder.Resources.
  • Ресурс webfrontend используется для вызова GetEnvironmentVariableValuesAsync для получения настроенных переменных среды.
  • Аргумент DistributedApplicationOperation.Publish передается при вызове GetEnvironmentVariableValuesAsync для указания переменных среды, опубликованных в ресурсе в качестве выражений привязки.
  • С возвращёнными переменными среды тест утверждает, что ресурс webfrontend имеет переменную среды HTTPS, которая разрешается в ресурс apiservice.

Сводка

Шаблон проекта тестирования .NET Aspire упрощает создание тестовых проектов для решений .NET Aspire. Проект шаблона включает пример теста, который можно использовать в качестве отправной точки для тестов. DistributedApplicationTestingBuilder следует знакомому шаблону WebApplicationFactory<TEntryPoint> в ASP.NET Core. Он позволяет создать тестовый узел для распределенного приложения и выполнить тесты с ним.

Наконец, при использовании DistributedApplicationTestingBuilder все журналы ресурсов перенаправляются на DistributedApplication по умолчанию. Перенаправление журналов ресурсов позволяет сценариям, в которых требуется подтвердить правильность ведения журнала ресурса.

См. также