In this article, we are going to test our HttpClient instance in .NET6 application by mocking it using the XUnit.
Now install the 'FluentAssertion' NuGet Package in the xUnit project.
Create An API And Unit Test Project:
Let's create a .Net6 Web API and xUnit sample applications to accomplish our demo. We can use either Visual Studio 2022 or Visual Studio Code(using .NET CLI commands) to create any.Net6 application. For this demo, I'm using the 'Visual Studio Code'(using the .NET CLI command) editor.
Create a folder where we want to set up our applications. Then add a solution file by running the below command.
Command To Add Solution File:
dotnet new sln -n Name_of_your_solution_file
dotnet new sln -n Name_of_your_solution_file
Now let's create a .NET6 Web API project.
dotnet new webapi -o Name_of_your_API_project
Now let's create xUnit project.
dotnet new xunit -o Name_of_your_API_project
Now add our API and xUnit project to our solution file
dotnet sln {your_solution}.sln add {Your_API_Project_Folder}/{Your_API_ProjectName}.csproj
dotnet sln {your_solution}.sln add {Your_xUnit_Project_Folder}/{Your_xUnit_ProjectName}.csproj
dotnet sln {your_solution}.sln add {Your_xUnit_Project_Folder}/{Your_xUnit_ProjectName}.csproj
Add the API project reference to the Test project.
dotnet add {Your_xUnit_Project_Folder}/{Your_xUnit_ProjectName}.csproj reference {Your_API_Project_Folder}/{Your_API_ProjectName}.csproj
xUnit Project Folder Structure:
Install Required NuGet Packages:
Now install the 'Moq' NuGet Package in the xUnit project.
.NET CLI Command
dotnet add package Moq --version 4.18.1
dotnet add package Moq --version 4.18.1
Package Manager
Install-Package Moq -Version 4.18.1
Install-Package Moq -Version 4.18.1
Now install the 'FluentAssertion' NuGet Package in the xUnit project.
.NET CLI Command
dotnet add package FluentAssertions --version 6.7.0
dotnet add package FluentAssertions --version 6.7.0
Package Manager
Install-Package FluentAssertions -Version 6.7.0
Install-Package FluentAssertions -Version 6.7.0
Test Case To Mock HttpClient:
In an API project, implemented a service to consume an external API using the HttpClient instance, let's try to write the test case for that service by mocking the HttpClient.
So let's understand the logic inside of the API project first.
Api_Project/Services/Client1Service.cs:
using Dot6.HttpMock.Api.Models; namespace Dot6.HttpMock.Api.Services; public class Client1Service: IClient1Service { private readonly HttpClient _httpClient; public Client1Service(HttpClient httpClient) { _httpClient = httpClient; } public async Task<PublicApiContainer> Get() { return await _httpClient.GetFromJsonAsync<PublicApiContainer>("/entries"); } }
- Here is our simple 'Client1Service' that consumes a third-party API.
- (Line: 7-11) Injected the 'HttpClient' instance into the constructor of the 'Client1Service'.
- (Line: 15) The 'GetFromJsonAsync()' method invokes third-party endpoint. Here is one thing we have to remember 'GetFromJsonAsync()' or any other API invoking methods internal calls the 'SendAsync' or 'Send' protected method.
builder.Services.AddHttpClient<IClient1Service, Client1Service>(options => { options.BaseAddress = new Uri("https://api.publicapis.org"); });
- Here register our 'Client1Service' with the 'AddHttpClient' service that will help to inject the 'HttpClient' instance into the 'Client1Service' constructor. Also, register the base address of the third-party endpoint.
Now our target is to write a test case for the 'Get()' method in the 'Client1Service'.
Let's mock the 'HttpMessageHandler'. So let's create a file like 'HttpClientHelper.cs' in the 'Helper' folder.
xUnit_Project/Helper/HttpClientHelper.cs:
using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Moq; using Moq.Protected; using Newtonsoft.Json; namespace Dot6.HttpMock.Test.Helpers; public class HttpClientHelper { public static Mock<HttpMessageHandler> GetResults<T>(T response) { var mockResponse = new HttpResponseMessage() { Content = new StringContent(JsonConvert.SerializeObject(response)), StatusCode = HttpStatusCode.OK }; mockResponse.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); var mockHandler = new Mock<HttpMessageHandler>(); mockHandler .Protected() .Setup<Task<HttpResponseMessage>>( "SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .ReturnsAsync(mockResponse); return mockHandler; } }
- Here 'GetResults()' method returns a mocked HttpMessageHandler with mock response data.
- (Line: 14)The 'GetResults()' takes mocked response as generic input parameter.
- (Line: 16-20) Initialized the 'HttpResponseMessage' with stringfied response and 200(ok) status.
- (Line: 22&23) Defined the content type.
- (Line: 27-33) Recall code from API_Project like 'GetFromJsonAsync()' which internally invokes the 'SenAsync' protected. Here we going to mock the 'SendAsync' method with our mocked 'HttpResponseMessage' as output. Here 'ItExpr' matches any input parameters of a protected method like 'SendAsync'.
xUnit_Project/MockData/PublicApiMock.cs:
using System.Collections.Generic; using Dot6.HttpMock.Api.Models; namespace Dot6.HttpMock.Test.MockData; public class PublicApiMock { public static PublicApiContainer Get() { return new PublicApiContainer { Count = 1, Entries = new List<PublicApi> { new PublicApi { API = "AdoptAPet", Description = "Resource to help get pets adopted", Auth = "apiKey", HTTPS = true, Link = "https://www.adoptapet.com/public/apis/pet_list.html", Category = "Animals" } } }; } }
- Here are some sample mock data response
xUnit_Project/System/Services/TestClient1Service.cs:
using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Dot6.HttpMock.Api.Models; using Dot6.HttpMock.Api.Services; using Dot6.HttpMock.Test.Helpers; using Dot6.HttpMock.Test.MockData; using Moq; using Moq.Protected; using Xunit; namespace Dot6.HttpMock.Test.System.Services; public class TestClient1Service { [Fact] public async Task Get_ShouldReturnList() { /// Arrange var mockData = PublicApiMock.Get(); var mockHandler = HttpClientHelper .GetResults<PublicApiContainer>(mockData); var mockHttpClient = new HttpClient(mockHandler.Object); mockHttpClient.BaseAddress = new Uri("https://api.publicapis.org"); var client1Service = new Client1Service(mockHttpClient); /// Act var results = await client1Service.Get(); /// Assert Assert.NotNull(results); mockHandler .Protected() .Verify( "SendAsync", Times.Exactly(1), ItExpr.Is<HttpRequestMessage>( req => req.Method == HttpMethod.Get && req.RequestUri == new Uri("https://api.publicapis.org/entries")), ItExpr.IsAny<CancellationToken>() ); } }
- (Line: 17) The 'Fact' attribute makes the method a testable unit.
- (Line: 21) Fetching the mock response data of type 'PublicApiContainer'.
- (Line: 22&23) Fetching the mock of 'HttpMessageHandler'.
- (Line: 25) Initialized HttpClient instance bypassing our 'HttpMessageHandler' as input to it. Inside of the mocked 'HttpMessageHandler', we handled the 'SendAsync' method execution.
- (Line: 26) The base domain of the third-party endpoint.
- (Line: 28) Initialized the 'Client1Service' by passing the 'HttpClient' instance as input.
- (Line: 31) Invoking our actual test method that is 'Get()' in the 'Client1Service'. Since we mocked our 'HttpClient' we are going to receive our mock data as a response.
- (Line: 34) Checking whether response data is null or not.
- (Line: 36-45) So on mocking the 'HttpClient', to check the test more efficient way we have to call the 'MockMessageHandler.Protected().Verify()' method, here we can check whether our 'SendAsync' method is executed or not, and also we can verify the kind of request we invoked and also we can verify the requested URL.
Test Case To Mock IHttpClientFactory:
In the API project, implemented another service to consume the external API using the IHttpClientFactory, let's try to write the test case for the service by mocking the IHttpClientFactory.
So let's understand the logic inside of the API project.
Api_project/Services/Client2Service.cs:
using Dot6.HttpMock.Api.Models; namespace Dot6.HttpMock.Api.Services; public class Client2Service: IClient2Service { private readonly IHttpClientFactory _httpClientFactory; public Client2Service(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async Task<PublicApiContainer> Get() { var httpclient = _httpClientFactory.CreateClient("publicAPI"); return await httpclient.GetFromJsonAsync<PublicApiContainer>("/entries"); } }
- (Line: 7-11) Injected the 'IHttpClientFactory' into the 'Client2Service'.
- (Line: 15) Generating the 'HttpClient' instance based on the registered name in 'Program.cs'.
builder.Services.AddHttpClient("publicAPI", options => { options.BaseAddress = new Uri("https://api.publicapis.org"); });
- HttpClient registered with a name.
So let's create a new test file for services like 'Client2Service.cs'.
xUnit_Project/System/Services/Client2Service.cs:
using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Dot6.HttpMock.Api.Models; using Dot6.HttpMock.Api.Services; using Dot6.HttpMock.Test.Helpers; using Dot6.HttpMock.Test.MockData; using Moq; using Moq.Protected; using Xunit; namespace Dot6.HttpMock.Test.System.Services; public class TestClient2Service { [Fact] public async Task Get_ShouldResult() { /// Arrange var mockData = PublicApiMock.Get(); var mockHandler = HttpClientHelper .GetResults<PublicApiContainer>(mockData); var mockHttpClient = new HttpClient(mockHandler.Object); mockHttpClient.BaseAddress = new Uri("https://api.publicapis.org"); var mockHttpClientFactory = new Mock<IHttpClientFactory>(); mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>())) .Returns(mockHttpClient); var client1Service = new Client2Service(mockHttpClientFactory.Object); /// Act var results = await client1Service.Get(); /// Assert Assert.NotNull(results); mockHandler .Protected() .Verify( "SendAsync", Times.Exactly(1), ItExpr.Is<HttpRequestMessage>( req => req.Method == HttpMethod.Get && req.RequestUri == new Uri("https://api.publicapis.org/entries")), ItExpr.IsAny<CancellationToken>() ); } }
- (Line: 28) Mocked the IHttpClientFactory.
- (Line: 29&30) Here mocking the 'CreateClient' method of 'IHttpClientFactory' and also returns mocked HttpClient instance from it.
Support Me!
Buy Me A Coffee
PayPal Me
Video Session:
Wrapping Up:
Hopefully, I think this article delivered some useful information on Unit Testing in Asp.NetCore Web API using xUnit. by mocking the HttpClient instance. I love to have your feedback, suggestions, and better techniques in the comment section below.
Comments
Post a Comment