Hello everyone, in today’s post I would be talking about how to create a mock of the HttpClient class in C# using the awesome Moq library.
The code below makes an API call that returns a todo object. The CustomLogger method evaluates the todo and logs to the Console a statement based on if the todo object has the “completed” property set to true or false.
using System.Text.Json;
using System.Text.Json.Serialization;
// Top-Level Statements
var httpClient = new HttpClient {BaseAddress = new Uri("https://jsonplaceholder.typicode.com/todos/")};
var logger = new TodoCustomLogger(httpClient, 1);
await logger.CustomLogger();
// Todo class with json attributes
public class Todo
{
[JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("userId")] public int UserId { get; set; }
[JsonPropertyName("title")] public string Title { get; set; } = "";
[JsonPropertyName("completed")] public bool Completed { get; set; }
}
public class TodoCustomLogger
{
private readonly HttpClient _client;
public TodoCustomLogger(HttpClient client, int id)
{
_client = client;
Id = id;
}
private int Id { get; }
public async Task<string> CustomLogger()
{
var response = await _client.GetAsync(Convert.ToString(Id));
var responseBody = await response.Content.ReadAsStringAsync();
var deserialized = JsonSerializer.Deserialize<Todo>(responseBody)!;
return deserialized.Completed
? $"Your todo: {deserialized.Title} has been completed "
: $"Your todo: {deserialized.Title} is yet to be completed ";
}
}
When writing a unit test you might consider mocking the HttpClient class using the Moq library. The problem you’ll encounter is that you can’t create a mock of the HttpClient class and set up expectations.
This is because the HttpClient members don’t have the virtual accessor to override them and there is no interface like IHttpClient to implement it at the time of writing this.
The good thing is that the HttpClient class constructor has an overload that allows you to pass in a handler.
The handler allows you to configure the HttpClient class. You can configure the cookies, headers, and responses with the handler.
So with the handler, when writing the unit tests all I need to do is to configure the handler to my taste and pass it into the HttpClient, for full control over the system under test.
The code below is a custom handler:
public class CustomHandler : HttpClientHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage
{
Content = new StringContent(
"{ 'userId': 1, 'id': 38, 'title': 'fugiat veniam minus', 'completed': false }")
});
}
}
So if I pass in the “CustomHandler” class into the constructor of the HttpClient class I would be assured that the HttpClient would return the object below:
{
"userId": 1,
"id": 38,
"title": "fugiat veniam minus",
"completed": false
}
// Here is how to configure the HttpClient class with the handler
var client = new HttpClient(new CustomHandler())
To mock the HttpClientHandler using the Moq’s library you would have to use the protected method of the Mock class because the SendAsync method or the Send method in the HttpClientHandler is protected.
var todo = new Todo()
{
UserId = 1,
Id = 38,
Title = "code",
Completed = false
};
HttpResponseMessage responseMessage = new HttpResponseMessage
{ //The StringContent class creates the HTTP body and content headers
Content = new StringContent(JsonSerializer.Serialize(todo))
};
var moq = new Mock<HttpClientHandler>();
moq.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()).ReturnsAsync(responseMessage);
var handler = moq.Object;
var client = new HttpClient(handler);
I used the ItExpr instead of the It class to match the input because the “SendAsync” is a protected class.
To know more about mocking a protected class using the Moq library check this link.
Below is the full Test class :
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Moq.Protected;
//using Program;
namespace Tests;
[TestClass]
public class TodoCustomLoggerTests
{
private readonly string baseUri = "https://jsonplaceholder.typicode.com/todos/";
[TestMethod]
public void LogTodoIsCompleted()
{
//arrange
var todo = new Todo
{
UserId = 1,
Id = 38,
Title = "code",
Completed = true
};
var responseMessage = new HttpResponseMessage
{
Content = new StringContent(JsonSerializer.Serialize(todo))
};
var moq = new Mock<HttpClientHandler>();
moq.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()).ReturnsAsync(responseMessage);
var handler = moq.Object;
var client = new HttpClient(handler);
client.BaseAddress = new Uri(baseUri);
//act
var logger = new TodoCustomLogger(client, 1);
//assert
var expected = $"Your todo: {todo.Title} has been completed ";
Assert.AreEqual(expected, logger.CustomLogger().Result);
}
[TestMethod]
public void LogTodoIsNotCompleted()
{
//arrange
var todo = new Todo
{
UserId = 1,
Id = 38,
Title = "code",
Completed = false
};
var responseMessage = new HttpResponseMessage
{
Content = new StringContent(JsonSerializer.Serialize(todo))
};
var moq = new Mock<HttpClientHandler>();
moq.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()).ReturnsAsync(responseMessage);
var handler = moq.Object;
var client = new HttpClient(handler);
client.BaseAddress = new Uri(baseUri);
//act
var logger = new TodoCustomLogger(client, 1);
//assert
var expected = $"Your todo: {todo.Title} is yet to be completed ";
Assert.AreEqual(expected, logger.CustomLogger().Result);
}
}
The link to the repository of the code above can be seen here.
Thanks for reading 😅