콘텐츠로 이동

Manage the AppHost in tests

이 콘텐츠는 아직 번역되지 않았습니다.

Select your testing framework

When writing functional or integration tests with Aspire, managing the AppHost instance efficiently is crucial. The AppHost represents the full application environment and can be costly to create and tear down. This article explains how to manage the AppHost instance in your Aspire tests.

For writing tests with Aspire, you use the Aspire.Hosting.Testing NuGet package which contains some helper classes to manage the AppHost instance in your tests.

Use the DistributedApplicationTestingBuilder class

Section titled “Use the DistributedApplicationTestingBuilder class”

In the tutorial on writing your first test, you were introduced to the DistributedApplicationTestingBuilder class which can be used to create the AppHost instance:

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

The DistributedApplicationTestingBuilder.CreateAsync<T> method takes the AppHost project type as a generic parameter to create the AppHost instance. While this method is executed at the start of each test, it’s more efficient to create the AppHost instance once and share it across tests as the test suite grows.

With xUnit, you implement the IAsyncLifetime interface on the test class to support asynchronous initialization and disposal of the AppHost instance. The InitializeAsync method is used to create the AppHost instance before the tests are run and the DisposeAsync method disposes the AppHost once the tests are completed.

public class WebTests : IAsyncLifetime
{
private DistributedApplication _app;
public async Task InitializeAsync()
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AspireApp_AppHost>();
_app = await appHost.BuildAsync();
}
public async Task DisposeAsync() => await _app.DisposeAsync();
[Fact]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
// test code here
}
}

With MSTest, you use the ClassInitializeAttribute and ClassCleanupAttribute on static methods of the test class to provide the initialization and cleanup of the AppHost instance. The ClassInitialize method is used to create the AppHost instance before the tests are run and the ClassCleanup method disposes the AppHost instance once the tests are completed.

[TestClass]
public class WebTests
{
private static DistributedApplication _app;
[ClassInitialize]
public static async Task ClassInitialize(TestContext context)
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AspireApp_AppHost>();
_app = await appHost.BuildAsync();
}
[ClassCleanup]
public static async Task ClassCleanup() => await _app.DisposeAsync();
[TestMethod]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
// test code here
}
}

With NUnit, you use the OneTimeSetUp and OneTimeTearDown attributes on methods of the test class to provide the setup and teardown of the AppHost instance. The OneTimeSetUp method is used to create the AppHost instance before the tests are run and the OneTimeTearDown method disposes the AppHost instance once the tests are completed.

public class WebTests
{
private DistributedApplication _app;
[OneTimeSetUp]
public async Task OneTimeSetup()
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.AspireApp_AppHost>();
_app = await appHost.BuildAsync();
}
[OneTimeTearDown]
public async Task OneTimeTearDown() => await _app.DisposeAsync();
[Test]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
// test code here
}
}

By capturing the AppHost in a field when the test run is started, you can access it in each test without the need to recreate it, decreasing the time it takes to run the tests. Then, when the test run completes, the AppHost is disposed, which cleans up any resources that were created during the test run, such as containers.

You can access the arguments from your AppHost with the args parameter. Arguments are also passed to .NET’s configuration system, so you can override many configuration settings this way. In the following example, you override the environment by specifying it as a command line option:

var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.MyAppHost>(
[
"--environment=Testing"
]);

Other arguments can be passed to your AppHost Program and made available in your AppHost. In the next example, you pass an argument to the AppHost and use it to control whether you add data volumes to a Postgres instance.

In the AppHost Program, you use configuration to support enabling or disabling volumes:

var postgres = builder.AddPostgres("postgres1");
if (builder.Configuration.GetValue("UseVolumes", true))
{
postgres.WithDataVolume();
}

In test code, you pass "UseVolumes=false" in the args to the AppHost:

public async Task DisableVolumesFromTest()
{
// Disable volumes in the test builder via arguments:
using var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.TestingAppHost1_AppHost>(
[
"UseVolumes=false"
]);
// The container will have no volume annotation since we disabled volumes by passing UseVolumes=false
var postgres = builder.Resources.Single(r => r.Name == "postgres1");
Assert.Empty(postgres.Annotations.OfType<ContainerMountAnnotation>());
}

Use the DistributedApplicationFactory class

Section titled “Use the DistributedApplicationFactory class”

While the DistributedApplicationTestingBuilder class is useful for many scenarios, there might be situations where you want more control over starting the AppHost, such as executing code before the builder is created or after the AppHost is built. In these cases, you implement your own version of the DistributedApplicationFactory class. This is what the DistributedApplicationTestingBuilder uses internally.

public class TestingAspireAppHost()
: DistributedApplicationFactory(typeof(Projects.AspireApp_AppHost))
{
// override methods here
}

The constructor requires the type of the AppHost project reference as a parameter. Optionally, you can provide arguments to the underlying host application builder. These arguments control how the AppHost starts and provide values to the args variable used by the AppHost.cs file to start the AppHost instance.

The DistributionApplicationFactory class provides several lifecycle methods that can be overridden to provide custom behavior throughout the preparation and creation of the AppHost. The available methods are OnBuilderCreating, OnBuilderCreated, OnBuilding, and OnBuilt.

For example, we can use the OnBuilderCreating method to set configuration, such as the subscription and resource group information for Azure, before the AppHost is created and any dependent Azure resources are provisioned, resulting in our tests using the correct Azure environment.

public class TestingAspireAppHost()
: DistributedApplicationFactory(typeof(Projects.AspireApp_AppHost))
{
protected override void OnBuilderCreating(
DistributedApplicationOptions applicationOptions,
HostApplicationBuilderSettings hostOptions)
{
hostOptions.Configuration ??= new();
hostOptions.Configuration["environment"] = "Development";
hostOptions.Configuration["AZURE_SUBSCRIPTION_ID"] = "00000000-0000-0000-0000-000000000000";
hostOptions.Configuration["AZURE_RESOURCE_GROUP"] = "my-resource-group";
}
}

Because of the order of precedence in the .NET configuration system, the environment variables will be used over anything in the appsettings.json or secrets.json file.

Another scenario you might want to use in the lifecycle is to configure the services used by the AppHost. In the following example, consider a scenario where you override the OnBuilderCreated API to add resilience to the HttpClient:

protected override void OnBuilderCreated(
DistributedApplicationBuilder applicationBuilder)
{
applicationBuilder.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddStandardResilienceHandler();
});
}
질문 & 답변협업커뮤니티토론보기