Skip to content

Commit f60c424

Browse files
author
Ajay kumar
committed
[Blog] [Ajay]: Add handling external services
1 parent db64a96 commit f60c424

6 files changed

Lines changed: 302 additions & 6 deletions

File tree

TestArena/Blog/BlogHome.razor

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,11 @@
3232
Date="@DateTime.Today.Date.ToLongDateString()" Category="Integration testing"
3333
BlogUrl="/blog/integration-testing-in-dotnet-with-database" />
3434
</div>
35+
36+
<div class="col">
37+
<Thumbnail ImageUrl="images/blog/integration-testing/handling-external-services/banner.png"
38+
Title="Integration testing for dotnet core APIs: Handling 3rd party service calls using wiremock" Author="Ajay kumar"
39+
Date="@DateTime.Today.Date.ToLongDateString()" Category="Integration testing"
40+
BlogUrl="/blog/integration-testing-in-dotnet-with-external-services" />
41+
</div>
3542
</div>

TestArena/Blog/IntegrationTesting/handling-database/Index.razor

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,10 @@ public class SuperHeroController(ISuperHeroRepository superHeroRepository)
5656

5757
<Section Heading="Summary" Level="4">
5858
<p>Welcome to the 2nd post in our Integration testing series. You may check out the previous post that introduces the concept of writing integration tests using <b>WebApplicationFactory</b> in dotnet below:</p>
59-
<BlogReferenceCard
60-
Title="Integration testing for dotnet core APIs: Introduction"
61-
Description="Integration testing for dotnet core APIs: Introduction"
62-
Url="/blog/integration-testing-in-dotnet-intro"
63-
ImageUrl="/images/blog/integration-testing/intro/banner.png"
64-
Source="devcodex.in"/>
59+
<BlogReferenceCard Title="Integration testing for dotnet core APIs: Introduction"
60+
Description="Integration testing for dotnet core APIs: Introduction"
61+
Url="/blog/integration-testing-in-dotnet-intro" ImageUrl="/images/blog/integration-testing/intro/banner.png"
62+
Source="devcodex.in" />
6563

6664
<p>Almost every application relies on persistent storage, typically through a database. Integration testing with a real database can be challenging, especially when trying to maintain isolation and consistency across tests. In this post, we will explore how to effectively manage database dependencies in integration tests using <b>WebApplicationFactory</b> and containerized databases.</p>
6765
<p><b>Pre-requisites</b>, in case you want to follow the same setup on your system:</p>
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
@page "/blog/integration-testing-in-dotnet-with-external-services"
2+
@using TestArena.Blog.Common
3+
4+
5+
<BlogContainer>
6+
<Header Title="Integration testing for dotnet core APIs: Handling 3rd party service calls using wiremock"
7+
Image="/images/blog/integration-testing/handling-external-services/banner.png" PublishedOn="@DateTime.Now"
8+
Authors="Ajay Kumar">
9+
</Header>
10+
11+
<Section Heading="What are integration tests in context of APIs?" Level="4">
12+
<p>In the context of <b>.NET Core APIs</b> for a blog, integration tests are automated tests that evaluate the
13+
functionality of various parts of your application working together as a whole. Specifically, these tests
14+
ensure that multiple components — such as controllers, database access, middleware, internal and external
15+
services — function correctly when integrated, as opposed to functioning correctly only in isolation (which
16+
would be covered by unit tests).</p>
17+
<p>Here is a quick one-liner summary on popular kinds of testing for an API codebase:</p>
18+
<ul>
19+
<li><b>Unit Tests:</b> Validate individual components like controllers or services in isolation.</li>
20+
<li><b>Integration Tests:</b> Verify the interaction between multiple components (e.g., API, database,
21+
middleware) as a cohesive system.</li>
22+
<li><b>Contract Tests:</b> Ensure that API endpoints conform to agreed-upon interfaces or expectations
23+
between services.</li>
24+
</ul>
25+
<p>Integration tests will help you in identifying possible bugs introduced due to any new changes in your code.
26+
</p>
27+
28+
<BlogReferenceCard Title="Integration testing for dotnet core APIs: Introduction"
29+
Description="Integration testing for dotnet core APIs: Introduction"
30+
Url="/blog/integration-testing-in-dotnet-intro" ImageUrl="/images/blog/integration-testing/intro/banner.png"
31+
Source="devcodex.in" />
32+
33+
<BlogImage ImagePath="/images/blog/integration-testing/intro/Build process when integration tests fail.webp"
34+
Description="Build process when integration tests fail" Number="1" />
35+
36+
<CodeSnippet Description="APIs for SuperHero" Number="1">
37+
[ApiController]
38+
[Route("[controller]")]
39+
public class SuperHeroController(ISuperHeroRepository superHeroRepository)
40+
: ControllerBase
41+
{
42+
[HttpGet("")]
43+
public async Task&lt;IEnumerable&lt;SuperHero&gt;&gt; Get()
44+
{
45+
return await superHeroRepository.GetAllSuperHeroes();
46+
}
47+
48+
[HttpGet("{id}")]
49+
public async Task&lt;IActionResult&gt; GetById(int id)
50+
{
51+
var superHero = await superHeroRepository.GetSuperHeroById(id);
52+
if (superHero == null)
53+
{
54+
return NotFound();
55+
}
56+
57+
return Ok(superHero);
58+
}
59+
}
60+
</CodeSnippet>
61+
</Section
62+
63+
<Section Heading="Summary" Level="4">
64+
<p>This is a continuation of the <b>Integration testing in dotnet</b> series. This time we will be covering the
65+
scenario where third-party service calls are present in our API flows.</p>
66+
<p>Before we begin further, you may want to visit the previous articles in this series to get a better context
67+
of integration testing in general and the code repository that we will be using for demo purposes here. The
68+
links to the articles are mentioned below:</p>
69+
<BlogReferenceCard Title="Getting started" Description="Integration testing for dotnet core APIs: Introduction"
70+
Url="/blog/integration-testing-in-dotnet-intro" ImageUrl="/images/blog/integration-testing/intro/banner.png"
71+
Source="medium.com" />
72+
<BlogReferenceCard Title="Demo with database"
73+
Description="Integration testing for dotnet core APIs: Handling database"
74+
Url="/blog/integration-testing-in-dotnet-database"
75+
ImageUrl="/images/blog/integration-testing/handling-database/banner.png" Source="medium.com" />
76+
<p>Also, all the code used here is available at:</p>
77+
<BlogReferenceCard Title="GitHub Repository" Description="Code for Integration Testing with 3rd Party Service"
78+
Url="https://github.com/ajaysskumar/SuperHeroSolution/tree/main/DemoWith3rdPartyService"
79+
ImageUrl="/images/blog/integration-testing/github/banner.png" Source="github.com" />
80+
</Section>
81+
82+
<Section Heading="Why need to mock?" Level="4">
83+
<p>Mocking third-party service calls is crucial to ensure tests are isolated, reliable, and repeatable.</p>
84+
<p>It avoids dependency on external systems that may be unavailable, slow, or unpredictable, allowing the focus
85+
to remain on verifying the behavior of your application in a controlled environment.</p>
86+
<p>Mocking also simplifies setting up specific scenarios, such as handling failures or edge cases, which might
87+
be hard to replicate with real services.</p>
88+
<p>However, unlike unit tests, the mocking here is wire-level mocking since we want all our logic, including the
89+
way we call the HTTP service and the way we parse the response, because here we are testing the overall
90+
integration of all the components of our application. So we will be mocking only what goes out to the
91+
network and what comes into the network here.</p>
92+
</Section>
93+
94+
<Section Heading="Suspects API" Level="4">
95+
<p>Continuing with the article, let's imagine we need an API that returns the list of notorious suspects in the
96+
superhero universe.</p>
97+
<p>We created an API as mentioned in the below Swagger screenshot:</p>
98+
99+
<BlogImage ImagePath="/images/blog/integration-testing/handling-external-services/Suspect API screenshot.webp"
100+
Description="Suspect API screenshot" Number="2" />
101+
102+
<CodeSnippet Description="Suspect API" Number="2">
103+
[HttpGet("/suspects")]
104+
public async Task&lt;IActionResult&gt; Suspects([FromQuery] string searchTerm)
105+
{
106+
var people = await GetPeople();
107+
var peopleOfInterest = people.Data.Where(person =>
108+
person.First_Name.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase) ||
109+
person.Last_Name.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase) ||
110+
person.Email.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase));
111+
if (!peopleOfInterest.Any())
112+
{
113+
return NotFound();
114+
}
115+
return Ok(peopleOfInterest);
116+
}
117+
118+
// Method to fetch people from 3rd party service
119+
// We have also defined a variable "SuspectServiceUrl" in appsettings.json
120+
private async Task&lt;PersonResponse&gt; GetPeople()
121+
{
122+
using var client = new HttpClient();
123+
124+
var url = $"{configuration.GetValue&lt;string&gt;("SuspectServiceUrl")}/api/users?page=1";
125+
var response = await client.GetAsync(url);
126+
if (!response.IsSuccessStatusCode)
127+
{
128+
throw new HttpRequestException($"Unable to get people from {url}");
129+
}
130+
131+
return await response.Content.ReadFromJsonAsync&lt;PersonResponse&gt;();
132+
}
133+
</CodeSnippet>
134+
</Section>
135+
136+
<Section Heading="Wiremock to the rescue!" Level="4">
137+
<p>Here we will be setting up <b>Wiremock</b> to be used in the integration tests. For those who are new to
138+
Wiremock, please visit <a href="https://github.com/WireMock-Net/WireMock.Net"
139+
target="_blank">WireMock.Net</a> for more details.</p>
140+
<p>Install the below package in the Integration test project:</p>
141+
<CodeSnippet Description="Install WireMock.Net" Number="3">
142+
dotnet add package WireMock.Net --version 1.6.10
143+
</CodeSnippet>
144+
<p>After setup, the shared fixture class will look as follows. I have provided comments to understand the code
145+
context better.</p>
146+
<p>On a high level, we did the following:</p>
147+
<ul>
148+
<li>Created and initialized the Wiremock server.</li>
149+
<li>Captured the mocked server’s base URL in the property <code>SuspectServiceUrlOverride</code>.</li>
150+
<li>Exposed the mocked server via the property <code>WireMockServer</code>. This is needed because our
151+
application can behave differently based on different responses from the third-party service.</li>
152+
</ul>
153+
<CodeSnippet Description="SharedFixture class" Number="4">
154+
public class SharedFixture : IAsyncLifetime
155+
{
156+
public string SuspectServiceUrlOverride { get; private set; } = null!;
157+
private WireMockServer? _server;
158+
159+
public WireMockServer WireMockServer => _server;
160+
161+
public async Task InitializeAsync()
162+
{
163+
SuspectServiceUrlOverride = StartWireMockForService();
164+
}
165+
166+
public async Task DisposeAsync()
167+
{
168+
_server?.Stop();
169+
}
170+
171+
private string StartWireMockForService()
172+
{
173+
_server = WireMockServer.Start();
174+
return _server.Urls[0];
175+
}
176+
}
177+
</CodeSnippet>
178+
<p>Next, we guide our application to override the <code>SuspectServiceUrl</code> from
179+
<code>appsettings.json</code> and take the base URL value for the third-party service from the mocked
180+
server, i.e., <code>SharedFixture</code>’s <code>SuspectServiceUrlOverride</code> property.</p>
181+
<p>For this, we make changes in our <code>CustomApiFactory</code> class as shown below:</p>
182+
<CodeSnippet Description="CustomApiFactory class" Number="5">
183+
public class CustomApiFactory(SharedFixture sharedFixture) : WebApplicationFactory&lt;Program&gt;
184+
{
185+
protected override void ConfigureWebHost(IWebHostBuilder builder)
186+
{
187+
builder.ConfigureAppConfiguration((_, configBuilder) =>
188+
{
189+
configBuilder.AddInMemoryCollection(new Dictionary&lt;string, string&gt;
190+
{
191+
["SuspectServiceUrl"] = sharedFixture.SuspectServiceUrlOverride
192+
});
193+
});
194+
}
195+
}
196+
</CodeSnippet>
197+
<p>This completes the setup. We are now ready to write the tests.</p>
198+
</Section>
199+
200+
<Section Heading="Test Scenarios" Level="4">
201+
<p>Below are the test scenarios for our Suspect API. These tests ensure that the API behaves as expected under
202+
different conditions, including both positive and negative cases.</p>
203+
204+
<CodeSnippet Description="SetupServiceMockForSuspectApi method" Number="6">
205+
/// &lt;summary&gt;
206+
/// This method will take params and will return the list of people/suspects
207+
/// &lt;/summary&gt;
208+
/// &lt;param name="pageNum"&gt;Number of page to look for. Can be any number, but for this problem, lets
209+
assume this will always be 1&lt;/param&gt;
210+
/// &lt;param name="apiResponse"&gt;Response JSON returned by the API&lt;/param&gt;
211+
/// &lt;param name="expectedStatusCode"&gt;Status code returned from the API&lt;/param&gt;
212+
/// &lt;typeparam name="T"&gt;Type of the response&lt;/typeparam&gt;
213+
private void SetupServiceMockForSuspectApi&lt;T&gt;(string pageNum, T apiResponse, HttpStatusCode
214+
expectedStatusCode = HttpStatusCode.OK)
215+
{
216+
factory.SharedFixture.WireMockServer
217+
.Given(Request
218+
.Create()
219+
.WithPath("/api/users")
220+
.UsingGet()
221+
.WithParam("page", MatchBehaviour.AcceptOnMatch, ignoreCase: true, pageNum))
222+
.RespondWith(Response
223+
.Create()
224+
.WithStatusCode(expectedStatusCode)
225+
.WithBodyAsJson(apiResponse, Encoding.UTF8));
226+
}
227+
</CodeSnippet>
228+
229+
<CodeSnippet Description="Positive Test: Valid Data" Number="7">
230+
[Fact(DisplayName = "Get suspects should return all matching suspects")]
231+
public async Task Get_All_Suspects_Returns_List_Of_Matching_Suspects()
232+
{
233+
// Arrange
234+
// Setting up mocked data for success response
235+
SetupServiceMockForSuspectApi("1", new PersonResponse()
236+
{
237+
Data =
238+
[
239+
new Suspect()
240+
{
241+
Id = 1,
242+
First_Name = "Selina",
243+
Last_Name = "Kyle",
244+
Email = "selina.kyle@gotham.com",
245+
}
246+
]
247+
});
248+
249+
// Act
250+
var response = await factory.CreateClient().GetAsync("/suspects?searchTerm=selina");
251+
252+
// Assert
253+
response.StatusCode.Should().Be(HttpStatusCode.OK);
254+
var superHeroes = await response.Content.ReadFromJsonAsync&lt;List&lt;Suspect&gt;&gt;();
255+
superHeroes.Should().NotBeEmpty();
256+
superHeroes![0].Id.Should().Be(1);
257+
superHeroes![0].Email.Should().Be("selina.kyle@gotham.com");
258+
superHeroes![0].First_Name.Should().Be("Selina");
259+
superHeroes![0].Last_Name.Should().Be("Kyle");
260+
}
261+
</CodeSnippet>
262+
263+
<CodeSnippet Description="Negative Test: 500 Response" Number="8">
264+
[Fact(DisplayName = "Get suspects should return 500 status code when API responds with 500 status code")]
265+
public async Task Get_All_Suspects_Should_Return_500_StatusCode_When_3rd_Party_Api_Fails()
266+
{
267+
// Arrange
268+
// Setting up mocked data for failure response
269+
SetupServiceMockForSuspectApi("1", new {Status = "Failed" }, HttpStatusCode.InternalServerError);
270+
271+
// Act
272+
var response = await factory.CreateClient().GetAsync("/suspects?searchTerm=selina");
273+
274+
// Assert
275+
response.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
276+
}
277+
</CodeSnippet>
278+
</Section>
279+
280+
<Section Heading="Test Results" Level="4">
281+
<p>When running the tests, we can observe the following results:</p>
282+
<BlogImage ImagePath="/images/blog/integration-testing/handling-external-services/Test results.webp"
283+
Description="Test results showing successful execution of all test cases" Number="3" />
284+
<p>As seen in the above screenshot, all the test cases have passed successfully, ensuring that our API behaves
285+
as expected under different scenarios, including both positive and negative cases.</p>
286+
</Section>
287+
288+
<p>Thats about it for this article. Hope you liked it.</p>
289+
290+
<EndNotes RepositoryLink="https://github.com/ajaysskumar/SuperHeroSolution" />
291+
</BlogContainer>
18.5 KB
Loading
55.5 KB
Loading
1.84 MB
Loading

0 commit comments

Comments
 (0)