Skip to content

Commit 1373e14

Browse files
authored
Create functional tests (#1217)
1 parent b7ef661 commit 1373e14

24 files changed

+812
-188
lines changed

tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs

Lines changed: 5 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public class DistributedApplicationTests
1818
{
1919
private readonly ITestOutputHelper _testOutputHelper;
2020

21-
// Primary constructors don't get ITestOutputHelper injected
2221
public DistributedApplicationTests(ITestOutputHelper testOutputHelper)
2322
{
2423
_testOutputHelper = testOutputHelper;
@@ -187,109 +186,6 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C
187186
}
188187
}
189188

190-
[LocalOnlyFact]
191-
public async Task TestProjectStartsAndStopsCleanly()
192-
{
193-
var testProgram = CreateTestProgram();
194-
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));
195-
196-
testProgram.AppBuilder.Services
197-
.AddHttpClient()
198-
.ConfigureHttpClientDefaults(b =>
199-
{
200-
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
201-
});
202-
203-
await using var app = testProgram.Build();
204-
205-
var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();
206-
207-
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
208-
209-
await app.StartAsync(cts.Token);
210-
211-
// Make sure each service is running
212-
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
213-
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
214-
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);
215-
}
216-
217-
[LocalOnlyFact]
218-
public async Task TestPortOnServiceBindingAnnotationAndAllocatedEndpointAnnotationMatch()
219-
{
220-
var testProgram = CreateTestProgram();
221-
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));
222-
223-
testProgram.AppBuilder.Services
224-
.AddHttpClient()
225-
.ConfigureHttpClientDefaults(b =>
226-
{
227-
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
228-
});
229-
230-
await using var app = testProgram.Build();
231-
232-
var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();
233-
234-
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
235-
236-
await app.StartAsync(cts.Token);
237-
238-
// Make sure each service is running
239-
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
240-
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
241-
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);
242-
243-
foreach (var projectBuilders in testProgram.ServiceProjectBuilders)
244-
{
245-
var serviceBinding = projectBuilders.Resource.Annotations.OfType<ServiceBindingAnnotation>().Single();
246-
var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType<AllocatedEndpointAnnotation>().Single();
247-
248-
Assert.Equal(serviceBinding.Port, allocatedEndpoint.Port);
249-
}
250-
}
251-
252-
[LocalOnlyFact]
253-
public async Task TestPortOnServiceBindingAnnotationAndAllocatedEndpointAnnotationMatchForReplicatedServices()
254-
{
255-
var testProgram = CreateTestProgram();
256-
257-
foreach (var serviceBuilder in testProgram.ServiceProjectBuilders)
258-
{
259-
serviceBuilder.WithReplicas(2);
260-
}
261-
262-
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));
263-
264-
testProgram.AppBuilder.Services
265-
.AddHttpClient()
266-
.ConfigureHttpClientDefaults(b =>
267-
{
268-
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
269-
});
270-
271-
await using var app = testProgram.Build();
272-
273-
var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();
274-
275-
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
276-
277-
await app.StartAsync(cts.Token);
278-
279-
// Make sure each service is running
280-
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
281-
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
282-
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);
283-
284-
foreach (var projectBuilders in testProgram.ServiceProjectBuilders)
285-
{
286-
var serviceBinding = projectBuilders.Resource.Annotations.OfType<ServiceBindingAnnotation>().Single();
287-
var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType<AllocatedEndpointAnnotation>().Single();
288-
289-
Assert.Equal(serviceBinding.Port, allocatedEndpoint.Port);
290-
}
291-
}
292-
293189
[LocalOnlyFact]
294190
public async Task TestServicesWithMultipleReplicas()
295191
{
@@ -315,6 +211,11 @@ public async Task TestServicesWithMultipleReplicas()
315211

316212
await app.StartAsync(cts.Token);
317213

214+
// Give the server some time to be ready to handle requests to
215+
// minimize the amount of retries the clients have to do (and log).
216+
217+
await Task.Delay(1000, cts.Token);
218+
318219
// Make sure services A and C are running
319220
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
320221
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);
@@ -336,67 +237,6 @@ public async Task TestServicesWithMultipleReplicas()
336237
}
337238
}
338239

339-
[LocalOnlyFact]
340-
public async Task VerifyHealthyOnIntegrationServiceA()
341-
{
342-
var testProgram = CreateTestProgram(includeIntegrationServices: true);
343-
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));
344-
345-
testProgram.AppBuilder.Services
346-
.AddHttpClient()
347-
.ConfigureHttpClientDefaults(b =>
348-
{
349-
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
350-
});
351-
352-
await using var app = testProgram.Build();
353-
354-
var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();
355-
356-
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
357-
358-
await app.StartAsync(cts.Token);
359-
360-
// Make sure all services are running
361-
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
362-
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
363-
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);
364-
await testProgram.IntegrationServiceABuilder!.HttpGetPidAsync(client, "http", cts.Token);
365-
366-
// We wait until timeout for the /health endpoint to return successfully. We assume
367-
// that components wired up into this project have health checks enabled.
368-
await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatus(client, "http", cts.Token);
369-
}
370-
371-
[LocalOnlyFact("node")]
372-
public async Task VerifyNodeAppWorks()
373-
{
374-
var testProgram = CreateTestProgram(includeNodeApp: true);
375-
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));
376-
377-
testProgram.AppBuilder.Services
378-
.AddHttpClient()
379-
.ConfigureHttpClientDefaults(b =>
380-
{
381-
b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5));
382-
b.AddStandardResilienceHandler();
383-
});
384-
385-
await using var app = testProgram.Build();
386-
387-
var client = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();
388-
389-
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
390-
391-
await app.StartAsync(cts.Token);
392-
393-
var response0 = await testProgram.NodeAppBuilder!.HttpGetWithRetryAsync(client, "http", "/", cts.Token);
394-
var response1 = await testProgram.NpmAppBuilder!.HttpGetWithRetryAsync(client, "http", "/", cts.Token);
395-
396-
Assert.Equal("Hello from node!", response0);
397-
Assert.Equal("Hello from node!", response1);
398-
}
399-
400240
[LocalOnlyFact("docker")]
401241
public async Task VerifyDockerAppWorks()
402242
{

tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,89 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
namespace Aspire.Hosting.Tests.Helpers;
5+
56
public static class AllocatedEndpointAnnotationTestExtensions
67
{
7-
public static async Task<string> HttpGetAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken)
8+
/// <summary>
9+
/// Sends a GET request to the specified resource and returns the response body as a string.
10+
/// </summary>
11+
/// <typeparam name="T">The type of the resource.</typeparam>
12+
/// <param name="builder">The resource.</param>
13+
/// <param name="client">The <see cref="HttpClient"/> instance to use.</param>
14+
/// <param name="bindingName">The name of the binding.</param>
15+
/// <param name="path">The path the request is sent to.</param>
16+
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
17+
/// <returns>The string representing the response body.</returns>
18+
public static async Task<string> HttpGetStringAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken)
819
where T : IResourceWithBindings
920
{
10-
// We have to get the allocated endpoint each time through the loop
11-
// because it may not be populated yet by the time we get here.
1221
var allocatedEndpoint = builder.Resource.Annotations.OfType<AllocatedEndpointAnnotation>().Single(a => a.Name == bindingName);
1322
var url = $"{allocatedEndpoint.UriString}{path}";
1423

1524
var response = await client.GetStringAsync(url, cancellationToken);
1625
return response;
1726
}
1827

19-
public static Task<string> WaitForHealthyStatus(this IResourceBuilder<ProjectResource> builder, HttpClient client, string bindingName, CancellationToken cancellationToken)
28+
/// <summary>
29+
/// Sends a GET request to the specified resource and returns the response message.
30+
/// </summary>
31+
/// <typeparam name="T">The type of the resource.</typeparam>
32+
/// <param name="builder">The resource.</param>
33+
/// <param name="client">The <see cref="HttpClient"/> instance to use.</param>
34+
/// <param name="bindingName">The name of the binding.</param>
35+
/// <param name="path">The path the request is sent to.</param>
36+
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
37+
/// <returns>The response message.</returns>
38+
public static async Task<HttpResponseMessage> HttpGetAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken)
39+
where T : IResourceWithBindings
2040
{
21-
return HttpGetWithRetryAsync(builder, client, bindingName, "/health", cancellationToken);
41+
var allocatedEndpoint = builder.Resource.Annotations.OfType<AllocatedEndpointAnnotation>().Single(a => a.Name == bindingName);
42+
var url = $"{allocatedEndpoint.UriString}{path}";
43+
44+
var response = await client.GetAsync(url, cancellationToken);
45+
return response;
46+
}
47+
48+
/// <summary>
49+
/// Sends a POST request to the specified resource and returns the response message.
50+
/// </summary>
51+
/// <typeparam name="T">The type of the resource.</typeparam>
52+
/// <param name="builder">The resource.</param>
53+
/// <param name="client">The <see cref="HttpClient"/> instance to use.</param>
54+
/// <param name="bindingName">The name of the binding.</param>
55+
/// <param name="path">The path the request is sent to.</param>
56+
/// <param name="content">The HTTP request content sent to the server.</param>
57+
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
58+
/// <returns>The response message.</returns>
59+
public static async Task<HttpResponseMessage> HttpPostAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string path, HttpContent? content, CancellationToken cancellationToken)
60+
where T : IResourceWithBindings
61+
{
62+
var allocatedEndpoint = builder.Resource.Annotations.OfType<AllocatedEndpointAnnotation>().Single(a => a.Name == bindingName);
63+
var url = $"{allocatedEndpoint.UriString}{path}";
64+
65+
var response = await client.PostAsync(url, content, cancellationToken);
66+
return response;
67+
}
68+
69+
public static Task<string> WaitForHealthyStatusAsync(this IResourceBuilder<ProjectResource> builder, HttpClient client, string bindingName, CancellationToken cancellationToken)
70+
{
71+
return HttpGetStringWithRetryAsync(builder, client, bindingName, "/health", cancellationToken);
2272
}
2373

2474
public static Task<string> HttpGetPidAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, CancellationToken cancellationToken)
2575
where T : IResourceWithBindings
2676
{
27-
return HttpGetWithRetryAsync(builder, client, bindingName, "/pid", cancellationToken);
77+
return HttpGetStringWithRetryAsync(builder, client, bindingName, "/pid", cancellationToken);
2878
}
2979

30-
public static async Task<string> HttpGetWithRetryAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string request, CancellationToken cancellationToken)
80+
public static async Task<string> HttpGetStringWithRetryAsync<T>(this IResourceBuilder<T> builder, HttpClient client, string bindingName, string request, CancellationToken cancellationToken)
3181
where T : IResourceWithBindings
3282
{
3383
while (true)
3484
{
3585
try
3686
{
37-
return await builder.HttpGetAsync(client, bindingName, request, cancellationToken);
38-
}
39-
catch (HttpRequestException ex)
40-
{
41-
Console.WriteLine(ex);
87+
return await builder.HttpGetStringAsync(client, bindingName, request, cancellationToken);
4288
}
4389
catch
4490
{

tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public override string Skip
1212
{
1313
get
1414
{
15+
// BUILD_BUILDID is defined by Azure Dev Ops
16+
1517
if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null)
1618
{
1719
return "LocalOnlyFactAttribute tests are not run as part of CI.";
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.Tests.Helpers;
5+
using Xunit;
6+
7+
namespace Aspire.Hosting.Tests;
8+
9+
[Collection("IntegrationServices")]
10+
public class IntegrationServicesTests
11+
{
12+
private readonly IntegrationServicesFixture _integrationServicesFixture;
13+
14+
public IntegrationServicesTests(IntegrationServicesFixture integrationServicesFixture)
15+
{
16+
_integrationServicesFixture = integrationServicesFixture;
17+
}
18+
19+
[LocalOnlyFact]
20+
public async Task VerifyHealthyOnIntegrationServiceA()
21+
{
22+
var testProgram = _integrationServicesFixture.TestProgram;
23+
var client = _integrationServicesFixture.HttpClient;
24+
25+
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
26+
27+
// Make sure all services are running
28+
await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token);
29+
await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token);
30+
await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token);
31+
await testProgram.IntegrationServiceABuilder!.HttpGetPidAsync(client, "http", cts.Token);
32+
33+
// We wait until timeout for the /health endpoint to return successfully. We assume
34+
// that components wired up into this project have health checks enabled.
35+
await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatusAsync(client, "http", cts.Token);
36+
}
37+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.Tests.Helpers;
5+
using Xunit;
6+
7+
namespace Aspire.Hosting.Tests.MongoDB;
8+
9+
[Collection("IntegrationServices")]
10+
public class MongoDBFunctionalTests
11+
{
12+
private readonly IntegrationServicesFixture _integrationServicesFixture;
13+
14+
public MongoDBFunctionalTests(IntegrationServicesFixture integrationServicesFixture)
15+
{
16+
_integrationServicesFixture = integrationServicesFixture;
17+
}
18+
19+
[LocalOnlyFact()]
20+
public async Task VerifyMongoWorks()
21+
{
22+
var testProgram = _integrationServicesFixture.TestProgram;
23+
var client = _integrationServicesFixture.HttpClient;
24+
25+
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
26+
27+
var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/verify", cts.Token);
28+
var responseContent = await response.Content.ReadAsStringAsync();
29+
30+
Assert.True(response.IsSuccessStatusCode, responseContent);
31+
}
32+
}

0 commit comments

Comments
 (0)