Skip to content

Commit 20611f7

Browse files
committed
[Blog] [Ajay]: Add integration testing with AWS blog
1 parent ad51c8c commit 20611f7

File tree

5 files changed

+254
-0
lines changed

5 files changed

+254
-0
lines changed

TestArena/Blog/Common/NavigationUtils/SiteMap.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ public static class SiteMap
6363
new DateTime(2025, 4, 19),
6464
"images/blog/react/module-federation/banner.png",
6565
["micro-fronted", "react", "module-federation"]),
66+
new("Integration testing for dotnet core APIs: Working with AWS flows",
67+
"/blog/integration-testing-in-dotnet-with-aws",
68+
new DateTime(2025, 5, 03),
69+
"images/blog/integration-testing/handling-aws/banner.png",
70+
["Integration Testing", "Localstack", "AWS"]),
6671
];
6772
}
6873

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
@page "/blog/integration-testing-in-dotnet-with-aws"
2+
@using TestArena.Blog.Common
3+
@using TestArena.Blog.Common.NavigationUtils
4+
5+
@code{
6+
PageInfo currentPage = SiteMap.Pages.FirstOrDefault(x => x.RelativePath == "/blog/integration-testing-in-dotnet-with-aws");
7+
}
8+
9+
<BlogContainer>
10+
<Header Title="@currentPage.Header"
11+
Image="@currentPage.ArticleImage" PublishedOn="@currentPage.PublishedOn" Authors="Ajay Kumar">
12+
</Header>
13+
<h4>Summary</h4>
14+
<p>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.</p>
15+
16+
<BlogReferenceCard Title="Integration testing for dotnet core APIs: Introduction"
17+
Description="Integration testing for dotnet core APIs: Introduction"
18+
Url="/blog/integration-testing-in-dotnet-intro" ImageUrl="/images/blog/integration-testing/intro/banner.png"
19+
Source="devcodex.in" />
20+
21+
<h6>Modern apps and cloud</h6>
22+
<p>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.</p>
23+
24+
<h6>Challenges in testing cloud integrations</h6>
25+
<p>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.</p>
26+
27+
<h6>Lets dive into the article</h6>
28+
<p>To make this more fun, imagine we’ve built an API called <code>call-superhero</code>. 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!</p>
29+
30+
<CodeSnippet Number="1" Description="Call superhero API to send message to SNS">
31+
[HttpPost("call-superhero")]
32+
public async Task&lt;IActionResult&gt; CallSuperHero(string superHeroName)
33+
{
34+
// Simulate calling a superhero
35+
var superHero = await superHeroRepository.GetAllSuperHeroes();
36+
var hero = superHero.FirstOrDefault(h => h.SuperName.Equals(superHeroName, StringComparison.InvariantCultureIgnoreCase));
37+
38+
// Publish an SNS notification
39+
var topicArn = configuration.GetValue&lt;string&gt;("AWS:SnsTopicArn"); // Ensure this is configured in appsettings.json
40+
var message = $"Calling {hero.SuperName}! They are on their way to save the day!";
41+
var publishRequest = new Amazon.SimpleNotificationService.Model.PublishRequest
42+
{
43+
TopicArn = topicArn,
44+
Message = message,
45+
Subject = "Superhero Alert"
46+
};
47+
48+
var response = await snsClient.PublishAsync(publishRequest);
49+
if (response.HttpStatusCode != System.Net.HttpStatusCode.OK)
50+
{
51+
return StatusCode((int)response.HttpStatusCode, "Failed to send SNS notification.");
52+
}
53+
54+
return Ok($"Calling {hero.SuperName}! They are on their way to save the day!");
55+
}
56+
</CodeSnippet>
57+
58+
<p>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!</p>
59+
<p>Here’s a quick recap of the AWS setup we did to make this work:</p>
60+
<ul>
61+
<li>Defined appsettings for AWS, including the region and the SNS topic ARN.</li>
62+
<li>Set up credentials for the application. Check out the <a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html" target="_blank">official AWS documentation</a> for configuring access key ID and secret access key on your local machine.</li>
63+
<li>Registered the dependency for <code>IAmazonSimpleNotificationService</code>.</li>
64+
</ul>
65+
66+
<h5>🔧 Configuring appsettings.json</h5>
67+
<p>Ensure your <code>appsettings.json</code> file includes the following configuration for AWS:</p>
68+
<CodeSnippet Number="2" Description="appsettings.json configuration">
69+
{
70+
"AWS": {
71+
"Region": "us-east-1",
72+
"SnsTopicArn": "arn:aws:sns:us-east-1:123456789012:dev-superhero-called"
73+
}
74+
}
75+
</CodeSnippet>
76+
<p><i>Note: To retrieve the SNS Topic ARN, create a topic in the AWS Management Console or using the AWS CLI. Refer to the <a href="https://docs.aws.amazon.com/sns/latest/dg/sns-create-topic.html" target="_blank">AWS documentation</a> for details.</i></p>
77+
78+
<h4>Setting Up LocalStack for Integration Testing</h4>
79+
<p>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 <a href="https://testcontainers.com/modules/localstack/" target="_blank">Testcontainers.LocalStack documentation</a>.</p>
80+
81+
<h5>Define the LocalStack Container</h5>
82+
<p>The <code>LocalStackContainer</code> is configured using the <code>Testcontainers.LocalStack</code> library. This setup specifies the Docker image, wait strategy, and cleanup options.</p>
83+
84+
<CodeSnippet Number="3" Description="Building LocalStack container">
85+
private readonly LocalStackContainer _localStackContainer =
86+
new LocalStackBuilder()
87+
.WithImage("localstack/localstack")
88+
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Ready."))
89+
.WithCleanUp(true)
90+
.WithAutoRemove(true)
91+
.Build();
92+
</CodeSnippet>
93+
94+
<h5>Start the LocalStack Container</h5>
95+
<p>The container is started asynchronously in the <code>InitializeAsync</code> method to ensure it’s ready before running tests.</p>
96+
97+
<CodeSnippet Number="4" Description="Starting LocalStack container">
98+
await _localStackContainer.StartAsync();
99+
</CodeSnippet>
100+
101+
<h5>Create an SNS Topic</h5>
102+
<p>We create an SNS topic using the <code>awslocal sns create-topic</code> command inside the LocalStack container. The output is parsed to extract the <code>TopicArn</code>.</p>
103+
104+
<CodeSnippet Number="5" Description="Creating SNS topic">
105+
private async Task&lt;string?&gt; CreateSnsTopic()
106+
{
107+
var createTopicResult = await _localStackContainer.ExecAsync(
108+
[
109+
"awslocal", "sns", "create-topic", "--name", "dev-superhero-called"
110+
]);
111+
112+
var createTopicOutput = createTopicResult.Stdout;
113+
var topicArn = JsonNode.Parse(createTopicOutput)?["TopicArn"]?.ToString();
114+
if (string.IsNullOrEmpty(topicArn))
115+
{
116+
throw new InvalidOperationException("Failed to create SNS topic in LocalStack.");
117+
}
118+
119+
return topicArn;
120+
}
121+
</CodeSnippet>
122+
123+
<h5>Expose the LocalStack Container</h5>
124+
<p>The <code>LocalStackContainer</code> is exposed as a property for use in tests, allowing additional AWS CLI commands to be executed.</p>
125+
126+
<CodeSnippet Number="6" Description="Exposing LocalStack container object">
127+
public LocalStackContainer LocalStackContainer => _localStackContainer;
128+
</CodeSnippet>
129+
130+
<h5>Clean Up Resources</h5>
131+
<p>Once the tests are done, the container is stopped and cleaned up to ensure no leftover resources.</p>
132+
133+
<CodeSnippet Number="7" Description="Cleaning up LocalStack container">
134+
public async Task DisposeAsync()
135+
{
136+
await _localStackContainer.DisposeAsync();
137+
}
138+
</CodeSnippet>
139+
140+
<p>With this setup, LocalStack is ready to emulate AWS services for integration testing. By using <code>LocalStackContainer.ExecAsync</code>, 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.</p>
141+
142+
<h4>LocalStack Setup in CustomApiFactory</h4>
143+
<p>The <code>CustomApiFactory</code> 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.</p>
144+
145+
<h5>Replace the SNS Client with LocalStack</h5>
146+
<p>The existing <code>IAmazonSimpleNotificationService</code> client is replaced with a LocalStack SNS client. This ensures that all SNS operations during tests are directed to LocalStack.</p>
147+
148+
<CodeSnippet Number="8" Description="Null check for snsClient">
149+
builder.ConfigureServices(services =>
150+
{
151+
var snsClient = services.SingleOrDefault(d => d.ServiceType == typeof(IAmazonSimpleNotificationService));
152+
if (snsClient != null)
153+
{
154+
services.Remove(snsClient);
155+
}
156+
services.AddSingleton(GetSnsClient(new Uri(SharedFixture.LocalStackContainer.GetConnectionString())));
157+
});
158+
</CodeSnippet>
159+
160+
<p>The <code>GetSnsClient</code> method creates an SNS client configured to use the LocalStack endpoint. This client is used for all SNS operations during tests.</p>
161+
162+
<CodeSnippet Number="9" Description="Method to create LocalStack SNS client">
163+
private IAmazonSimpleNotificationService GetSnsClient(Uri serviceUrl)
164+
{
165+
var credentials = new BasicAWSCredentials("keyId", "secret");
166+
var clientConfig = new AmazonSimpleNotificationServiceConfig
167+
{
168+
ServiceURL = serviceUrl.ToString()
169+
};
170+
return new AmazonSimpleNotificationServiceClient(credentials, clientConfig);
171+
}
172+
</CodeSnippet>
173+
174+
<h5>Override Application Configuration</h5>
175+
<p>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.</p>
176+
177+
<CodeSnippet Number="10" Description="Overriding application configuration">
178+
builder.ConfigureAppConfiguration((_, configBuilder) =>
179+
{
180+
Console.WriteLine($"SNS topic in config {sharedFixture.SnsTopicArn}");
181+
configBuilder.AddInMemoryCollection(new Dictionary&lt;string, string&gt;
182+
{
183+
["SuspectServiceUrl"] = sharedFixture.SuspectServiceUrlOverride,
184+
["AWS:SnsTopicArn"] = sharedFixture.SnsTopicArn
185+
}!);
186+
});
187+
</CodeSnippet>
188+
189+
<p>The <code>CustomApiFactory</code> 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.</p>
190+
191+
<h4>Finally, the Test!</h4>
192+
<p>The test verifies that the <code>CallSuperHero</code> 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.</p>
193+
194+
<CodeSnippet Number="11" Description="Test for CallSuperHero API">
195+
[Fact(DisplayName = "CallSuperHero API raises correct SNS notification using LocalStack")]
196+
public async Task CallSuperHero_Raises_Correct_SNS_Notification_Using_LocalStack()
197+
{
198+
// Arrange
199+
var superHeroName = "Venom";
200+
factory.SharedFixture.SuperHeroDbContext.SuperHero.AddRange(new List&lt;SuperHeroApiWith3rdPartyService.Data.Models.SuperHero&gt;()
201+
{
202+
new(5, "Venom", "Eddie Brock", "Super strength, Shape-shifting, Healing factor", "San Francisco", 35),
203+
});
204+
await factory.SharedFixture.SuperHeroDbContext.SaveChangesAsync();
205+
206+
// Create SQS queue and subscribe it to the SNS topic
207+
var queueName = "superhero-called-queue";
208+
var queue = await factory.SharedFixture.LocalStackContainer.ExecAsync(["awslocal", "sqs", "create-queue", "--queue-name", queueName]);
209+
var queueUrl = JsonNode.Parse(queue.Stdout)!["QueueUrl"]!.ToString();
210+
211+
var queueAttributeResult = await factory.SharedFixture.LocalStackContainer.ExecAsync(["awslocal", "sqs", "get-queue-attributes", "--queue-url", queueUrl, "--attribute-names", "All"]);
212+
213+
// Extract the QueueArn from the queue attributes
214+
var queueArn = JsonNode.Parse(queueAttributeResult.Stdout)!["Attributes"]!["QueueArn"]!.ToString();
215+
216+
// Subscribe the SQS queue to the SNS topic
217+
await factory.SharedFixture.LocalStackContainer.ExecAsync(["awslocal", "sns", "subscribe", "--topic-arn", factory.SharedFixture.SnsTopicArn, "--protocol", "sqs", "--notification-endpoint", queueArn]);
218+
219+
// Act
220+
var response = await factory.CreateClient().PostAsJsonAsync($"/SuperHero/call-superhero?superHeroName={superHeroName}", new { });
221+
222+
// Assert
223+
response.StatusCode.Should().Be(HttpStatusCode.OK);
224+
var responseMessage = await response.Content.ReadAsStringAsync();
225+
responseMessage.Should().Contain($"Calling {superHeroName}! They are on their way to save the day!");
226+
227+
// Verify SNS message was published
228+
var messages = await factory.SharedFixture.LocalStackContainer.ExecAsync(
229+
[
230+
"awslocal", "sqs", "receive-message", "--queue-url", queueUrl
231+
]);
232+
var sqsMessages = JsonNode.Parse(messages.Stdout);
233+
var message = sqsMessages!["Messages"]![0]!["Body"]!.ToString();
234+
message.Should().Contain($"Calling {superHeroName}! They are on their way to save the day!");
235+
}
236+
</CodeSnippet>
237+
238+
<BlogImage ImagePath="/images/blog/integration-testing/handling-aws/passing test.png"
239+
Description="Passing Test" Number="1" />
240+
241+
<p>This test demonstrates how to use LocalStack to verify that the <code>CallSuperHero</code> 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.</p>
242+
243+
<p>If you want to explore the actual code discussed in this article, feel free to visit the GitHub repository: <a href="https://github.com/ajaysskumar/SuperHeroSolution" target="_blank">https://github.com/ajaysskumar/SuperHeroSolution</a>. It contains the complete implementation and test setup for the <code>CallSuperHero</code> API and AWS integration testing using LocalStack.</p>
244+
</BlogContainer>
1.57 MB
Loading
149 KB
Loading

TestArena/wwwroot/sitemap.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,9 @@
5555
<lastmod>2025-04-19</lastmod>
5656
<changefreq>monthly</changefreq>
5757
</url>
58+
<url>
59+
<loc>https://devcodex.in/blog/integration-testing-in-dotnet-with-aws</loc>
60+
<lastmod>2025-05-03</lastmod>
61+
<changefreq>monthly</changefreq>
62+
</url>
5863
</urlset>

0 commit comments

Comments
 (0)