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)