Our application is an ASP.NET Core API, Postage DB using Entity Framework, and SQS Lambda handler. The main idea is to configure an application using Aspire for local development, and the same configuration for integration testing.
Solution structure
- ApiService - ASP.NET Core API that persists data to Postgres and sends a message to AWS SQS. EF Core code-first approach for persistent abstraction.
- Lambda - AWS Lambda that is triggered based on an SQS message
- AppHost - Describes and runs resources for an application.
- Tests - xUnit tests that use the Aspire app host to run and test
ApiService
andLambda
projects.
Aspire Configuration
- DB: Postgres container, PgAdmin container, and DB Migrate using
ef core
tool
// Postgres configuration var postgres = builder.AddPostgres(AspireResources.Postgres) .WithLifetime(ContainerLifetime.Persistent) .WithPgAdmin(c => c.WithLifetime(ContainerLifetime.Persistent)); var db = postgres.AddDatabase(AspireResources.PostgresDb); // DB migration var migrator = builder .AddExecutable( "ef-db-migrations", "dotnet", "../AspirePoc.ApiService", "ef", "database", "update", "--no-build" ) .WaitFor(db) .WithReference(db);
- AWS: LocalStack container, Lambda emulator, Lambda handler
// LocalStack configuration var awsConfig = builder.AddAWSSDKConfig() .WithRegion(RegionEndpoint.EUCentral1); var localStack = builder.AddLocalStack(AspireResources.LocalStack) .WithEnvironment("AWS_DEFAULT_REGION", awsConfig.Region!.SystemName); // AWS Lambda function configuration builder.AddAWSLambdaFunction<Projects.AspirePoc_Lambda>( AspireResources.Lambda, "AspirePoc.Lambda::AspirePoc.Lambda.Function::FunctionHandler") .WithReference(awsConfig);
- API: Api service with connection to DB and LocalStack
// API var url = $"http://sqs.eu-central-1.localhost.localstack.cloud:4566/000000000000/{AspireResources.LocalStackResources.SqsName}"; var apiService = builder.AddProject<Projects.AspirePoc_ApiService>(AspireResources.Api) .WithReference(localStack) .WithReference(awsConfig) .WithReference(db) .WithEnvironment("SqsUrl", url) .WaitForCompletion(migrator);
Integration test implementation
- xUnit fixture - Start up the Aspire test host before tests run
[assembly: AssemblyFixture(typeof(AspireFixture))] namespace AspirePoc.Tests.Infrastructure; public class AspireFixture : IAsyncLifetime { public IDistributedApplicationTestingBuilder AppHost { get; private set; } public DistributedApplication? App { get; private set; } public async ValueTask InitializeAsync() { AppHost = await DistributedApplicationTestingBuilder .CreateAsync<Projects.AspirePoc_ApiService>(); AppHost.Services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddStandardResilienceHandler(); }); // Remove useless resources for testing RemoveNotNeededResourcesForTesting(); // Change config for testing ModifyResourcesForTesting(); // Set AWS credentials for testing SetupTestDependencies(); App = await AppHost.BuildAsync(); await App.StartAsync(); // Wait for the API service to be healthy using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await App.ResourceNotifications .WaitForResourceHealthyAsync(AspireResources.Api, cts.Token); } public async ValueTask DisposeAsync() { if (App is not null) { await App.DisposeAsync(); } }
- Configure test host and dependencies(Http client, DbContext, LambdaClient) to arrange tests
AppHost.Services .AddSingleton<IAmazonLambda>(_ => new AmazonLambdaClient( new BasicAWSCredentials("mock-access-key", "mock-secret-access-key"), new AmazonLambdaConfig { ServiceURL = AppHost.GetLambdaEmulatorUrl(), })) .AddSingleton(_ => { var connectionString = AppHost.GetConnectionString() .GetAwaiter().GetResult(); var npqConnBuilder = new NpgsqlConnectionStringBuilder(connectionString) { IncludeErrorDetail = true }; var source = new NpgsqlDataSourceBuilder(npqConnBuilder.ToString()) .Build(); return new AppDbContext( new DbContextOptionsBuilder<AppDbContext>() .UseNpgsql(source) .Options); });
- API Test example
[Fact] public async Task Get_WeatherForecast_ShouldReturnResponse() { // Arrange dbContext.Add(new WeatherForecast(new DateOnly(), 25, "Sunny")); await dbContext.SaveChangesAsync(TestContext.Current.CancellationToken); // Act var response = await sut.GetFromJsonAsync<WeatherForecast[]>( "/weatherforecast", TestContext.Current.CancellationToken); // Assert response.ShouldNotBeEmpty(); }
- Lambda handler test example
[Fact] public async Task InvokeLambda_SQSLambda_ShouldAccepted() { var request = new InvokeRequest { FunctionName = AspireResources.Lambda, InvocationType = InvocationType.Event, Payload = @"{ ""Records"": [ { ""body"": ""Test message"" } ] }" }; // Act var response = await lambdaClient.InvokeAsync( request, TestContext.Current.CancellationToken); // Assert response.StatusCode.ShouldBe(202); }
Conclusion
The result appears in the Aspire dashboard and GitHub
Top comments (0)