Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create functional tests #1217

Merged
merged 22 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
344b6f9
Add smoke tests for mongodb
sebastienros Dec 6, 2023
1ccbfa0
Rename ConfigureMongoDB to MapMongoMovieApi
sebastienros Dec 6, 2023
76375dd
Use XUnit fixture to share same app host
sebastienros Dec 7, 2023
677295c
Don't run MongoDB code on startup
sebastienros Dec 7, 2023
30d7745
Update tests/Aspire.Hosting.Tests/TestProgramFixture.cs
sebastienros Dec 7, 2023
88c337f
Update tests/Aspire.Hosting.Tests/TestProgramFixture.cs
sebastienros Dec 7, 2023
6891e92
Make mongo apis idempotent
sebastienros Dec 7, 2023
a89ff93
Add remaining functional tests
sebastienros Dec 9, 2023
d06f701
Merge remote-tracking branch 'origin/main' into sebros/functionalmongo
sebastienros Dec 9, 2023
bdf78e2
Run functional tests on Linux
sebastienros Dec 11, 2023
ed4016c
Make database queries idempotent
sebastienros Dec 11, 2023
6c6ffa0
Disable runs on Linux CI
sebastienros Dec 11, 2023
6eb7a46
Remove unrelated loop comments
sebastienros Dec 11, 2023
33dc3b6
Simplify MongoDB tests
sebastienros Dec 11, 2023
3a5c29b
Test functional tests on windows
sebastienros Dec 11, 2023
950d2fe
Bool logic is hard
sebastienros Dec 11, 2023
6ba5814
Limit Console usage that would slow down TestServicesWithMultipleRepl…
sebastienros Dec 12, 2023
448651d
Merge branch 'main' into sebros/functionalmongo
sebastienros Dec 13, 2023
71a8ec3
Don't run on CI for now
sebastienros Dec 13, 2023
df7c3c7
Fix build
sebastienros Dec 13, 2023
24c7105
Add code comment
sebastienros Dec 13, 2023
b6ce547
Log problem details
sebastienros Dec 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 0 additions & 165 deletions tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ public class DistributedApplicationTests
{
private readonly ITestOutputHelper _testOutputHelper;

// Primary constructors don't get ITestOutputHelper injected
public DistributedApplicationTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
Expand Down Expand Up @@ -187,109 +186,6 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C
}
}

[LocalOnlyFact]
public async Task TestProjectStartsAndStopsCleanly()
{
var testProgram = CreateTestProgram();
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));

testProgram.AppBuilder.Services
.AddHttpClient()
.ConfigureHttpClientDefaults(b =>
{
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
});

await using var app = testProgram.Build();

var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));

await app.StartAsync(cts.Token);

// Make sure each service is running
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);
}

[LocalOnlyFact]
public async Task TestPortOnServiceBindingAnnotationAndAllocatedEndpointAnnotationMatch()
{
var testProgram = CreateTestProgram();
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));

testProgram.AppBuilder.Services
.AddHttpClient()
.ConfigureHttpClientDefaults(b =>
{
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
});

await using var app = testProgram.Build();

var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));

await app.StartAsync(cts.Token);

// Make sure each service is running
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);

foreach (var projectBuilders in testProgram.ServiceProjectBuilders)
{
var serviceBinding = projectBuilders.Resource.Annotations.OfType<ServiceBindingAnnotation>().Single();
var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType<AllocatedEndpointAnnotation>().Single();

Assert.Equal(serviceBinding.Port, allocatedEndpoint.Port);
}
}

[LocalOnlyFact]
public async Task TestPortOnServiceBindingAnnotationAndAllocatedEndpointAnnotationMatchForReplicatedServices()
{
var testProgram = CreateTestProgram();

foreach (var serviceBuilder in testProgram.ServiceProjectBuilders)
{
serviceBuilder.WithReplicas(2);
}

testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));

testProgram.AppBuilder.Services
.AddHttpClient()
.ConfigureHttpClientDefaults(b =>
{
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
});

await using var app = testProgram.Build();

var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));

await app.StartAsync(cts.Token);

// Make sure each service is running
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);

foreach (var projectBuilders in testProgram.ServiceProjectBuilders)
{
var serviceBinding = projectBuilders.Resource.Annotations.OfType<ServiceBindingAnnotation>().Single();
var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType<AllocatedEndpointAnnotation>().Single();

Assert.Equal(serviceBinding.Port, allocatedEndpoint.Port);
}
}

[LocalOnlyFact]
public async Task TestServicesWithMultipleReplicas()
{
Expand Down Expand Up @@ -336,67 +232,6 @@ public async Task TestServicesWithMultipleReplicas()
}
}

[LocalOnlyFact]
public async Task VerifyHealthyOnIntegrationServiceA()
{
var testProgram = CreateTestProgram(includeIntegrationServices: true);
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));

testProgram.AppBuilder.Services
.AddHttpClient()
.ConfigureHttpClientDefaults(b =>
{
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
});

await using var app = testProgram.Build();

var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));

await app.StartAsync(cts.Token);

// Make sure all services are running
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.IntegrationServiceABuilder!.HttpGetPidAsync(client, "http", cts.Token);

// We wait until timeout for the /health endpoint to return successfully. We assume
// that components wired up into this project have health checks enabled.
await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatus(client, "http", cts.Token);
}

[LocalOnlyFact("node")]
public async Task VerifyNodeAppWorks()
{
var testProgram = CreateTestProgram(includeNodeApp: true);
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));

testProgram.AppBuilder.Services
.AddHttpClient()
.ConfigureHttpClientDefaults(b =>
{
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
b.AddStandardResilienceHandler();
});

await using var app = testProgram.Build();

var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));

await app.StartAsync(cts.Token);

var response0 = await testProgram.NodeAppBuilder!.HttpGetWithRetryAsync(client, "http", "/", cts.Token);
var response1 = await testProgram.NpmAppBuilder!.HttpGetWithRetryAsync(client, "http", "/", cts.Token);

Assert.Equal("Hello from node!", response0);
Assert.Equal("Hello from node!", response1);
}

[LocalOnlyFact("docker")]
public async Task VerifyDockerAppWorks()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.Tests.Helpers;

public static class AllocatedEndpointAnnotationTestExtensions
{
public static async Task<string> HttpGetAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken)
/// <summary>
/// Sends a GET request to the specified resource and returns the response body as a string.
/// </summary>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <param name="builder">The resource.</param>
/// <param name="client">The <see cref="HttpClient"/> instance to use.</param>
/// <param name="bindingName">The name of the binding.</param>
/// <param name="path">The path the request is sent to.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>The string representing the response body.</returns>
public static async Task<string> HttpGetStringAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken)
where T : IResourceWithBindings
{
// We have to get the allocated endpoint each time through the loop
Expand All @@ -16,25 +27,70 @@ public static async Task<string> HttpGetAsync<T>(this IResourceBuilder<T> builde
return response;
}

public static Task<string> WaitForHealthyStatus(this IResourceBuilder<ProjectResource> builder, HttpClient client, string bindingName, CancellationToken cancellationToken)
/// <summary>
/// Sends a GET request to the specified resource and returns the response message.
/// </summary>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <param name="builder">The resource.</param>
/// <param name="client">The <see cref="HttpClient"/> instance to use.</param>
/// <param name="bindingName">The name of the binding.</param>
/// <param name="path">The path the request is sent to.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>The response message.</returns>
public static async Task<HttpResponseMessage> HttpGetAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken)
where T : IResourceWithBindings
{
// We have to get the allocated endpoint each time through the loop
// because it may not be populated yet by the time we get here.
var allocatedEndpoint = builder.Resource.Annotations.OfType<AllocatedEndpointAnnotation>().Single(a => a.Name == bindingName);
var url = $"{allocatedEndpoint.UriString}{path}";

var response = await client.GetAsync(url, cancellationToken);
return response;
}

/// <summary>
/// Sends a POST request to the specified resource and returns the response message.
/// </summary>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <param name="builder">The resource.</param>
/// <param name="client">The <see cref="HttpClient"/> instance to use.</param>
/// <param name="bindingName">The name of the binding.</param>
/// <param name="path">The path the request is sent to.</param>
/// <param name="content">The HTTP request content sent to the server.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>The response message.</returns>
public static async Task<HttpResponseMessage> HttpPostAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string path, HttpContent? content, CancellationToken cancellationToken)
where T : IResourceWithBindings
{
// We have to get the allocated endpoint each time through the loop
// because it may not be populated yet by the time we get here.
var allocatedEndpoint = builder.Resource.Annotations.OfType<AllocatedEndpointAnnotation>().Single(a => a.Name == bindingName);
var url = $"{allocatedEndpoint.UriString}{path}";

var response = await client.PostAsync(url, content, cancellationToken);
return response;
}

public static Task<string> WaitForHealthyStatusAsync(this IResourceBuilder<ProjectResource> builder, HttpClient client, string bindingName, CancellationToken cancellationToken)
{
return HttpGetWithRetryAsync(builder, client, bindingName, "/health", cancellationToken);
return HttpGetStringWithRetryAsync(builder, client, bindingName, "/health", cancellationToken);
}

public static Task<string> HttpGetPidAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, CancellationToken cancellationToken)
where T : IResourceWithBindings
{
return HttpGetWithRetryAsync(builder, client, bindingName, "/pid", cancellationToken);
return HttpGetStringWithRetryAsync(builder, client, bindingName, "/pid", cancellationToken);
}

public static async Task<string> HttpGetWithRetryAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string request, CancellationToken cancellationToken)
public static async Task<string> HttpGetStringWithRetryAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string request, CancellationToken cancellationToken)
where T : IResourceWithBindings
{
while (true)
{
try
{
return await builder.HttpGetAsync(client, bindingName, request, cancellationToken);
return await builder.HttpGetStringAsync(client, bindingName, request, cancellationToken);
}
catch (HttpRequestException ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public override string Skip
{
get
{
if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null)
// BUILD_BUILDID is defined by Azure Dev Ops

if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null && !OperatingSystem.IsLinux())
{
return "LocalOnlyFactAttribute tests are not run as part of CI.";
}
Expand Down
37 changes: 37 additions & 0 deletions tests/Aspire.Hosting.Tests/IntegrationServicesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.Tests.Helpers;
using Xunit;

namespace Aspire.Hosting.Tests;

[Collection("IntegrationServices")]
public class IntegrationServicesTests
{
private readonly IntegrationServicesFixture _integrationServicesFixture;

public IntegrationServicesTests(IntegrationServicesFixture integrationServicesFixture)
{
_integrationServicesFixture = integrationServicesFixture;
}

[LocalOnlyFact]
public async Task VerifyHealthyOnIntegrationServiceA()
{
var testProgram = _integrationServicesFixture.TestProgram;
var client = _integrationServicesFixture.HttpClient;

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));

// Make sure all services are running
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);
await testProgram.IntegrationServiceABuilder!.HttpGetPidAsync(client, "http", cts.Token);

// We wait until timeout for the /health endpoint to return successfully. We assume
// that components wired up into this project have health checks enabled.
await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatusAsync(client, "http", cts.Token);
}
}
47 changes: 47 additions & 0 deletions tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Http.Json;
using Aspire.Hosting.Tests.Helpers;
using Xunit;

namespace Aspire.Hosting.Tests.MongoDB;

[Collection("IntegrationServices")]
public class MongoDBFunctionalTests
{
private readonly IntegrationServicesFixture _integrationServicesFixture;

public MongoDBFunctionalTests(IntegrationServicesFixture integrationServicesFixture)
{
_integrationServicesFixture = integrationServicesFixture;
}

[LocalOnlyFact()]
public async Task DatabaseIsCreatedOnDemand()
{
var testProgram = _integrationServicesFixture.TestProgram;
var client = _integrationServicesFixture.HttpClient;

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));

var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/databases", cts.Token);
var databases = await response.Content.ReadFromJsonAsync<string[]>(cts.Token);

Assert.Equivalent(new[] { "admin", "config", "local", "mymongodb" }, databases);
}

[LocalOnlyFact()]
public async Task VerifyMongoWorks()
{
var testProgram = _integrationServicesFixture.TestProgram;
var client = _integrationServicesFixture.HttpClient;

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));

var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/movies", cts.Token);
var movies = await response.Content.ReadFromJsonAsync<string[]>(cts.Token);

Assert.Equivalent(new[] { "Rocky I", "Rocky II" }, movies);
}
}
Loading