Integration testing for dotnet core APIs: Working with AWS flows

Article Banner
Author(s): Ajay Kumar
Last updated: 03 May 2025

Summary

Welcome back to our integration testing in .NET series! In this chapter, we’re diving into the exciting world of testing AWS components integrated into our .NET APIs. For a quick introduction on integration testing, refer to the article below.

Integration testing for dotnet core APIs: Introduction

Integration testing for dotnet core APIs: Introduction

devcodex.in
Blog Image
Modern apps and cloud

AWS services (or any cloud services) have become the backbone of modern application development, offering scalable and reliable solutions for a variety of needs. For example, Amazon SNS (Simple Notification Service) sends notifications to users or systems via email, SMS, or other channels. Meanwhile, Amazon SQS (Simple Queue Service) enables asynchronous communication between distributed system components.

Challenges in testing cloud integrations

Here’s the twist: while using AWS services like SNS, SQS, S3, or SES is straightforward with the SDK, how do we ensure that our messages actually reach AWS? It’s not just about calling the right method with the right input; it’s about verifying that AWS receives and processes our requests correctly.

Another challenge is the cost associated with testing on real AWS infrastructure. Running integration tests frequently on live AWS services can quickly add up, especially when dealing with high volumes of requests or multiple environments. This is where tools like LocalStack come in handy, allowing us to emulate AWS services locally and avoid unnecessary expenses.

Lets dive into the article

To make this more fun, imagine we’ve built an API called call-superhero. This API takes a superhero’s name as a parameter and sends an SNS notification, hoping that the superhero hears the call and springs into action!


    [HttpPost("call-superhero")]
    public async Task<IActionResult> CallSuperHero(string superHeroName)
    {
        // Simulate calling a superhero
        var superHero = await superHeroRepository.GetAllSuperHeroes();
        var hero = superHero.FirstOrDefault(h => h.SuperName.Equals(superHeroName, StringComparison.InvariantCultureIgnoreCase));

        // Publish an SNS notification
        var topicArn = configuration.GetValue<string>("AWS:SnsTopicArn"); // Ensure this is configured in appsettings.json
        var message = $"Calling {hero.SuperName}! They are on their way to save the day!";
        var publishRequest = new Amazon.SimpleNotificationService.Model.PublishRequest
        {
            TopicArn = topicArn,
            Message = message,
            Subject = "Superhero Alert"
        };

        var response = await snsClient.PublishAsync(publishRequest);
        if (response.HttpStatusCode != System.Net.HttpStatusCode.OK)
        {
            return StatusCode((int)response.HttpStatusCode, "Failed to send SNS notification.");
        }

        return Ok($"Calling {hero.SuperName}! They are on their way to save the day!");
    }
    
Code Sample #1 : Call superhero API to send message to SNS

In the above code, we’re using the SNS client to publish a message to the SNS topic. This ensures that our superhero call is broadcasted loud and clear!

Here’s a quick recap of the AWS setup we did to make this work:

  • Defined appsettings for AWS, including the region and the SNS topic ARN.
  • Set up credentials for the application. Check out the official AWS documentation for configuring access key ID and secret access key on your local machine.
  • Registered the dependency for IAmazonSimpleNotificationService.
🔧 Configuring appsettings.json

Ensure your appsettings.json file includes the following configuration for AWS:


    {
        "AWS": {
            "Region": "us-east-1",
            "SnsTopicArn": "arn:aws:sns:us-east-1:123456789012:dev-superhero-called"
        }
    }
    
Code Sample #2 : appsettings.json configuration

Note: To retrieve the SNS Topic ARN, create a topic in the AWS Management Console or using the AWS CLI. Refer to the AWS documentation for details.

Setting Up LocalStack for Integration Testing

For more information, visit the official LocalStack website: https://localstack.cloud/.

LocalStack is like having your own personal AWS cloud running locally. Below is a step-by-step guide to setting it up for integration testing in a .NET application. For more details, refer to the Testcontainers.LocalStack documentation.

A quick glance on using localstack for testing vs using real AWS resources
Aspect LocalStack Real AWS
Cost ✅ Free or minimal cost for local testing. ❌ Can be expensive, especially for high-frequency testing.
Speed ✅ Faster as it runs locally without network latency. ❌ Slower due to network calls and AWS service response times.
Environment ✅ Runs locally, no dependency on internet or AWS account. ❌ Requires internet and a valid AWS account.
Feature Coverage ❌ Supports many AWS services but may lack full feature parity. ✅ Complete feature set and latest updates.
Realism ❌ Simulates AWS services but may not perfectly replicate behavior. ✅ Provides real-world behavior and interactions.
Setup Complexity ✅ Can be quickly spinned up using docker or local setup ❌ Requires AWS credentials and service configuration.
Scalability Testing ❌ Limited to local machine resources. ✅ Can test real-world scalability and performance.
Debugging ✅ Easier to debug locally with full control over the environment. ❌ Harder to debug due to remote infrastructure.

💡 LocalStack isn't just for testing; it also speeds up development by emulating AWS locally, reducing costs and dependency on internet connectivity while ensuring AWS API compatibility.

Define the LocalStack Container

The LocalStackContainer is configured using the Testcontainers.LocalStack library. This setup specifies the Docker image, wait strategy, and cleanup options.


    private readonly LocalStackContainer _localStackContainer =
        new LocalStackBuilder()
            .WithImage("localstack/localstack")
            .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Ready."))
            .WithCleanUp(true)
            .WithAutoRemove(true)
            .Build();
    
Code Sample #3 : Building LocalStack container

Start the LocalStack Container

The container is started asynchronously in the InitializeAsync method to ensure it’s ready before running tests.


    await _localStackContainer.StartAsync();
    
Code Sample #4 : Starting LocalStack container

Create an SNS Topic

We create an SNS topic using the awslocal sns create-topic command inside the LocalStack container. The output is parsed to extract the TopicArn.


    private async Task<string?> CreateSnsTopic()
    {
        var createTopicResult = await _localStackContainer.ExecAsync(
        [
            "awslocal", "sns", "create-topic", "--name", "dev-superhero-called"
        ]);

        var createTopicOutput = createTopicResult.Stdout;
        var topicArn = JsonNode.Parse(createTopicOutput)?["TopicArn"]?.ToString();
        if (string.IsNullOrEmpty(topicArn))
        {
            throw new InvalidOperationException("Failed to create SNS topic in LocalStack.");
        }

        return topicArn;
    }
    
Code Sample #5 : Creating SNS topic

Expose the LocalStack Container

The LocalStackContainer is exposed as a property for use in tests, allowing additional AWS CLI commands to be executed.


    public LocalStackContainer LocalStackContainer => _localStackContainer;
    
Code Sample #6 : Exposing LocalStack container object

Clean Up Resources

Once the tests are done, the container is stopped and cleaned up to ensure no leftover resources.


    public async Task DisposeAsync()
    {
        await _localStackContainer.DisposeAsync();
    }
    
Code Sample #7 : Cleaning up LocalStack container

With this setup, LocalStack is ready to emulate AWS services for integration testing. By using LocalStackContainer.ExecAsync, you can directly execute AWS CLI commands inside the LocalStack container, making it easy to manage AWS resources like SNS topics and SQS queues during tests.

LocalStack Setup in CustomApiFactory

The CustomApiFactory class is responsible for configuring the test environment, including setting up LocalStack for integration testing. Below are the steps and code snippets related to LocalStack setup in this class.

Replace the SNS Client with LocalStack

The existing IAmazonSimpleNotificationService client is replaced with a LocalStack SNS client. This ensures that all SNS operations during tests are directed to LocalStack.


    builder.ConfigureServices(services =>
    {
        var snsClient = services.SingleOrDefault(d => d.ServiceType == typeof(IAmazonSimpleNotificationService));
        if (snsClient != null)
        {
            services.Remove(snsClient);
        }
        services.AddSingleton(GetSnsClient(new Uri(SharedFixture.LocalStackContainer.GetConnectionString())));
    });
    
Code Sample #8 : Null check for snsClient

The GetSnsClient method creates an SNS client configured to use the LocalStack endpoint. This client is used for all SNS operations during tests.


    private IAmazonSimpleNotificationService GetSnsClient(Uri serviceUrl)
    {
        var credentials = new BasicAWSCredentials("keyId", "secret");
        var clientConfig = new AmazonSimpleNotificationServiceConfig
        {
            ServiceURL = serviceUrl.ToString()
        };
        return new AmazonSimpleNotificationServiceClient(credentials, clientConfig);
    }
    
Code Sample #9 : Method to create LocalStack SNS client

Override Application Configuration

The SNS topic ARN created in LocalStack is injected into the application configuration. This ensures that the application uses the correct topic ARN during tests.


    builder.ConfigureAppConfiguration((_, configBuilder) =>
    {
        Console.WriteLine($"SNS topic in config {sharedFixture.SnsTopicArn}");
        configBuilder.AddInMemoryCollection(new Dictionary<string, string>
        {
            ["SuspectServiceUrl"] = sharedFixture.SuspectServiceUrlOverride,
            ["AWS:SnsTopicArn"] = sharedFixture.SnsTopicArn
        }!);
    });
    
Code Sample #10 : Overriding application configuration

The CustomApiFactory class ensures that all SNS operations during integration tests are directed to LocalStack. By replacing the default SNS client and overriding the application configuration, it provides a seamless testing environment for AWS integrations.

Finally, the Test!

The test verifies that the CallSuperHero API correctly publishes a message to an SNS topic and that the message is received by an SQS queue subscribed to the topic. This test uses LocalStack to emulate AWS services.


    [Fact(DisplayName = "CallSuperHero API raises correct SNS notification using LocalStack")]
    public async Task CallSuperHero_Raises_Correct_SNS_Notification_Using_LocalStack()
    {
        // Arrange
        var superHeroName = "Venom";
        factory.SharedFixture.SuperHeroDbContext.SuperHero.AddRange(new List<SuperHeroApiWith3rdPartyService.Data.Models.SuperHero>()
        {
            new(5, "Venom", "Eddie Brock", "Super strength, Shape-shifting, Healing factor", "San Francisco", 35),
        });
        await factory.SharedFixture.SuperHeroDbContext.SaveChangesAsync();

        // Create SQS queue and subscribe it to the SNS topic
        var queueName = "superhero-called-queue";
        var queue = await factory.SharedFixture.LocalStackContainer.ExecAsync(["awslocal", "sqs", "create-queue", "--queue-name", queueName]);
        var queueUrl = JsonNode.Parse(queue.Stdout)!["QueueUrl"]!.ToString();

        var queueAttributeResult = await factory.SharedFixture.LocalStackContainer.ExecAsync(["awslocal", "sqs", "get-queue-attributes", "--queue-url", queueUrl, "--attribute-names", "All"]);

        // Extract the QueueArn from the queue attributes
        var queueArn = JsonNode.Parse(queueAttributeResult.Stdout)!["Attributes"]!["QueueArn"]!.ToString();

        // Subscribe the SQS queue to the SNS topic
        await factory.SharedFixture.LocalStackContainer.ExecAsync(["awslocal", "sns", "subscribe", "--topic-arn", factory.SharedFixture.SnsTopicArn, "--protocol", "sqs", "--notification-endpoint", queueArn]);

        // Act
        var response = await factory.CreateClient().PostAsJsonAsync($"/SuperHero/call-superhero?superHeroName={superHeroName}", new { });

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var responseMessage = await response.Content.ReadAsStringAsync();
        responseMessage.Should().Contain($"Calling {superHeroName}! They are on their way to save the day!");

        // Verify SNS message was published
        var messages = await factory.SharedFixture.LocalStackContainer.ExecAsync(
        [
          "awslocal", "sqs", "receive-message", "--queue-url", queueUrl
        ]);
        var sqsMessages = JsonNode.Parse(messages.Stdout);
        var message = sqsMessages!["Messages"]![0]!["Body"]!.ToString();
        message.Should().Contain($"Calling {superHeroName}! They are on their way to save the day!");
    }
    
Code Sample #11 : Test for CallSuperHero API

Passing Test
Figure 1 : Passing Test

This test demonstrates how to use LocalStack to verify that the CallSuperHero API correctly interacts with AWS SNS and SQS. By emulating AWS services locally or via test containers, the test ensures that the API behaves as expected without incurring costs or requiring access to real AWS resources.

If you want to explore the actual code discussed in this article, feel free to visit the GitHub repository: https://github.com/ajaysskumar/SuperHeroSolution. It contains the complete implementation and test setup for the CallSuperHero API and AWS integration testing using LocalStack.

Copyright © 2025 Dev Codex

An unhandled error has occurred. Reload 🗙