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 < ; IEnumerable < ; SuperHero > ;> ; Get ()
44+ {
45+ return await superHeroRepository .GetAllSuperHeroes ();
46+ }
47+
48+ [HttpGet (" {id}" )]
49+ public async Task < ; IActionResult > ; 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< ; IActionResult> ; 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< ; PersonResponse> ; 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 > ; ();
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< ; Program> ;
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+ /// < ; summary> ;
206+ /// This method will take params and will return the list of people/suspects
207+ /// < ; /summary> ;
208+ /// < ; param name="pageNum"> ; Number of page to look for. Can be any number, but for this problem, lets
209+ assume this will always be 1< ; /param> ;
210+ /// < ; param name="apiResponse"> ; Response JSON returned by the API< ; /param> ;
211+ /// < ; param name="expectedStatusCode"> ; Status code returned from the API< ; /param> ;
212+ /// < ; typeparam name="T"> ; Type of the response< ; /typeparam> ;
213+ private void SetupServiceMockForSuspectApi< ; T> ; (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 < ; Suspect > ;> ; ();
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 >
0 commit comments