DEV Community

Cover image for How to test HttpClient with Moq in C#
Gaute Meek Olsen
Gaute Meek Olsen

Posted on

How to test HttpClient with Moq in C#

This is how you can unit test your methods that use HttpClient with Moq and xUnit.

We don't want our unit tests to actually perform HTTP requests during testing so we will have to mock those requests. Moq allows us to mock overridable members such as abstract, virtual, or interface methods. But this doesn't exist in HttpClient. We could wrap HttpClient in an Interface, but that would result in extra implementation code and we don't want to alter implementation code to support tests. But if we look at the constructor, it takes in a HttpMessageHandler that contains an abstract SendAsync method that is used by HttpClient. This is what we want to mock!

Note that in HttpClient all GetAsync, PostAsync, PatchAsync, PutAsync, DeleteAsync, and SendAsync use the SendAsync method in the HttpMessageHandler internally and can be mocked.

Implementation to test

Here is an example of a Posts class which can fetch posts and create a post.

using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text.Json;

namespace HttpCode
{
    public class Posts
    {
        private readonly HttpClient httpClient;
        private const string url = "https://jsonplaceholder.typicode.com/posts";

        public Posts(HttpClient httpClient)
        {
            this.httpClient = httpClient;
        }

        public async Task<IEnumerable<JsonElement>> GetPosts()
        {
            var response = await httpClient.GetAsync(url);
            var body = await response.Content.ReadAsStringAsync();
            var posts = JsonSerializer.Deserialize<IEnumerable<JsonElement>>(body);
            return posts;
        }

        public async Task<JsonElement> CreatePost(string title)
        {
            var payload = new
            {
                title
            };
            var httpContent = new StringContent(JsonSerializer.Serialize(payload));
            var response = await httpClient.PostAsync(url, httpContent);
            var body = await response.Content.ReadAsStringAsync();
            var created = JsonSerializer.Deserialize<JsonElement>(body);
            return created;
        }
    }
}

Unit Tests

Now let's mock the SendAsync method to test our two methods. The SendAsync method is protected, so we need to use .Protected() and access the method with a string to be able to mock it.

using HttpCode;
using Moq;
using Moq.Protected;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace HttpCodeTests
{
    public class PostsTest
    {
        [Fact]
        public async void ShouldReturnPosts()
        {
            var handlerMock = new Mock<HttpMessageHandler>();
            var response = new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.OK,
                Content = new StringContent(@"[{ ""id"": 1, ""title"": ""Cool post!""}, { ""id"": 100, ""title"": ""Some title""}]"),
            };

            handlerMock
               .Protected()
               .Setup<Task<HttpResponseMessage>>(
                  "SendAsync",
                  ItExpr.IsAny<HttpRequestMessage>(),
                  ItExpr.IsAny<CancellationToken>())
               .ReturnsAsync(response);
            var httpClient = new HttpClient(handlerMock.Object);
            var posts = new Posts(httpClient);

            var retrievedPosts = await posts.GetPosts();

            Assert.NotNull(retrievedPosts);
            handlerMock.Protected().Verify(
               "SendAsync",
               Times.Exactly(1),
               ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
               ItExpr.IsAny<CancellationToken>());
        }

        [Fact]
        public async void ShouldCallCreatePostApi()
        {
            var handlerMock = new Mock<HttpMessageHandler>();
            var response = new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.OK,
                Content = new StringContent(@"{ ""id"": 101 }"),
            };
            handlerMock
               .Protected()
               .Setup<Task<HttpResponseMessage>>(
                  "SendAsync",
                  ItExpr.IsAny<HttpRequestMessage>(),
                  ItExpr.IsAny<CancellationToken>())
               .ReturnsAsync(response);
            var httpClient = new HttpClient(handlerMock.Object);
            var posts = new Posts(httpClient);

            var retrievedPosts = await posts.CreatePost("Best post");

            handlerMock.Protected().Verify(
               "SendAsync",
               Times.Exactly(1),
               ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Post),
               ItExpr.IsAny<CancellationToken>());
        }
    }
}

That's All

Hope this can help you as well. Happy coding!👨‍💻👩‍💻

Discussion (5)

Collapse
richardoey profile image
RichardOey

Hi, thank you very much for the tutorial ! It helps me to know how to test the SendAsync function.

However, I'm little confused with the assert action. Usually we will compare the unit test result with our expectation result, and we put inside the "assert" part. In your example, where do you put the assert? Thanks :)

Collapse
gautemeekolsen profile image
Gaute Meek Olsen Author

Hi thanks for the comment :)

In the first test you see I do the assert Assert.NotNull(retrievedPosts);, so it's there I would have it. Because my demo code GetPosts and CreatePost is simple it doesn't feel natural to add more assertions. But maybe one I could have added was Assert.Equal(2, retrievedPosts.Count); instead of Assert.NotNull(retrievedPosts); and there is where I would put it.

Collapse
msdickinson profile image
Mark Dickinson

Thank you, this helped me move forward in my unit testing :)

Collapse
bhaumikd profile image
Bhaumik Dhandhukia

Thanks for the explanation. It helped me create a test which verifies custom headers and apiUrl. Wondering if you can add how to verify req.content would be great.

Collapse
carlosdaniiel07 profile image
Carlos Daniel Martins de Almeida

Thank you, helped me a lot!