DEV Community

Cover image for Integration Testing with .NET Aspire: Unified Local and Test environments with SQS, Lambda, and Postgres
ohalay
ohalay

Posted on

Integration Testing with .NET Aspire: Unified Local and Test environments with SQS, Lambda, and Postgres

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 and Lambda 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);
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode

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();
    }
 }
Enter fullscreen mode Exit fullscreen mode
  • 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);
    });
Enter fullscreen mode Exit fullscreen mode
  • 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();
}
Enter fullscreen mode Exit fullscreen mode
  • 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);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The result appears in the Aspire dashboard and GitHub
Aspire dashboard

Top comments (0)