60

.NET Core 2.1 comes with this new factory called HttpClientFactory, but I can't figure out how to mock it to unit test some methods that include REST service calls.

The factory is being injected using .NET Core IoC container, and what the method does is create a new client from the factory:

var client = _httpClientFactory.CreateClient();

And then using the client to get data from a REST service:

var result = await client.GetStringAsync(url);
David Klempfner
  • 8,700
  • 20
  • 73
  • 153
Mauricio Atanache
  • 2,424
  • 1
  • 13
  • 18

7 Answers7

94

The HttpClientFactory is derived from IHttpClientFactory Interface So it is just a matter of creating a mock of the interface

var mockFactory = new Mock<IHttpClientFactory>();

Depending on what you need the client for, you would then need to setup the mock to return a HttpClient for the test.

This however requires an actual HttpClient.

var clientHandlerStub = new DelegatingHandlerStub();
var client = new HttpClient(clientHandlerStub);

mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);

IHttpClientFactory factory = mockFactory.Object;

The factory can then be injected into the dependent system under test when exercising the test.

If you do not want the client calling actual endpoints then you will need to create a fake delegate handler to intercept the requests.

Example of the handler stub used to fake the requests

public class DelegatingHandlerStub : DelegatingHandler {
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handlerFunc;
    public DelegatingHandlerStub() {
        _handlerFunc = (request, cancellationToken) => Task.FromResult(request.CreateResponse(HttpStatusCode.OK));
    }

    public DelegatingHandlerStub(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handlerFunc) {
        _handlerFunc = handlerFunc;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
        return _handlerFunc(request, cancellationToken);
    }
}

Taken from an answer I gave here

Reference Mock HttpClient using Moq

Suppose you have a controller

[Route("api/[controller]")]
public class ValuesController : Controller {
    private readonly IHttpClientFactory _httpClientFactory;

    public ValuesController(IHttpClientFactory httpClientFactory) {
        _httpClientFactory = httpClientFactory;
    }

    [HttpGet]
    public async Task<IActionResult> Get() {
        var client = _httpClientFactory.CreateClient();
        var url = "http://example.com";
        var result = await client.GetStringAsync(url);
        return Ok(result);
    }
}

and wanted to test the Get() action.

public async Task Should_Return_Ok() {
    //Arrange
    var expected = "Hello World";
    var mockFactory = new Mock<IHttpClientFactory>();
    var configuration = new HttpConfiguration();
    var clientHandlerStub = new DelegatingHandlerStub((request, cancellationToken) => {
        request.SetConfiguration(configuration);
        var response = request.CreateResponse(HttpStatusCode.OK, expected);
        return Task.FromResult(response);
    });
    var client = new HttpClient(clientHandlerStub);
    
    mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);
    
    IHttpClientFactory factory = mockFactory.Object;
    
    var controller = new ValuesController(factory);
    
    //Act
    var result = await controller.Get();
    
    //Assert
    result.Should().NotBeNull();
    
    var okResult = result as OkObjectResult;
    
    var actual = (string) okResult.Value;
    
    actual.Should().Be(expected);
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • 2
    When I tried to do something like this with Moq, I got an error from Moq not supporting setup for extension methods, this is the reason why I asked in first instance, anyway, i’ll give a try to your version tomorrow. – Mauricio Atanache Jan 17 '19 at 02:39
  • 5
    @MauricioAtanache note that I setup the actual interface member and not the extension method. The extension will eventually call the interface method. – Nkosi Jan 17 '19 at 08:57
  • 14
    In case anyone is curious, I was having problems finding the appropriate references for `request.CreateResponse()` - which can be found in HttpRequestMessageExtensions. I added this to my unit test project by adding the Microsoft.AspNet.WebApi.Core package to the project – Carter Musick Apr 11 '19 at 22:18
  • 4
    Wow. What a pain... I wish the `CreateClient()` method returned an interface instead of the concrete HttpClient type. – Tom Faltesek Jul 03 '19 at 15:50
  • 1
    You should change `.Returns(client)` to `.Returns(() => new HttpClient(clientHandlerStub))` in order to prevent that disposed `HttpClient`s are returned on the second call. – ditzel Oct 15 '19 at 12:16
  • 1
    @ditzel while good advice, there should be no second call in this instance. This answer targets the one expected call in an isolated unit test. If the target under test requires multiple clients being created then yes, the function delegate would be used in setting up the test. Thank you for your valid observation – Nkosi Oct 15 '19 at 12:30
  • @Nkosi, can you provide more insight on how you "setup the actual interface member and not the extension method"? – scottctr Jan 22 '20 at 18:46
  • 1
    @scottctr if you look at the [interface definition](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.ihttpclientfactory?view=dotnet-plat-ext-3.1) you will see that it has a single instance member that takes a string, but there is also an extension method that does not. The extension method calls the instance method so that is what needed to be setup. – Nkosi Jan 22 '20 at 19:07
  • @Nkosi How to use this for `SetupSequence`? `_mockHttpClientFactory.SetupSequence(_ => _.CreateClient(It.IsAny())).Returns(client1).Returns(client2);` is not working! how can I mock multiple times for different response? – TanvirArjel Jun 20 '20 at 13:01
  • Hi, I followed your answer but I am getting an exception because the baseAddress on my client is null. How can I setup the baseAddress in this case? – Fakhar Ahmad Rasul Apr 20 '21 at 17:48
  • @FakharAhmadRasul Just set the base address on the client. ie `client.BaseAddress = new Uri(".....")`. You are using an actual `HttpClient` instance – Nkosi Apr 20 '21 at 19:10
  • @Nkosi Worked! Thanks a lot for the help – Fakhar Ahmad Rasul Apr 21 '21 at 00:04
  • I'm getting the exception that FakharAhmadRasul had, but the solution to add a real URI seems strange to me. I wouldn't want to be accidentally calling an endpoint during tests. Is there a way to avoid having to set the `BaseAddress` on the client? – Lukas Oct 25 '21 at 15:59
  • 1
    @Lukas all you need is a properly formatted URL. You are using an actual HttpClient with a fake handler. The call will not be going to an actual endpoint. – Nkosi Oct 25 '21 at 17:55
  • Very interessting... Requires an actual HttpClient to then create a mock httpClient. I want to create an end to end test and actually have a httpClient created that actually calls the endpoint that is needed to reach the database. – Sebastiaan Haverkate Aug 09 '22 at 12:25
29

In addition to the previous post that describes how to setup a stub, you can just use Moq to setup the DelegatingHandler:

var clientHandlerMock = new Mock<DelegatingHandler>();
clientHandlerMock.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK))
    .Verifiable();
clientHandlerMock.As<IDisposable>().Setup(s => s.Dispose());

var httpClient = new HttpClient(clientHandlerMock.Object);

var clientFactoryMock = new Mock<IHttpClientFactory>(MockBehavior.Strict);
clientFactoryMock.Setup(cf => cf.CreateClient()).Returns(httpClient).Verifiable();

clientFactoryMock.Verify(cf => cf.CreateClient());
clientHandlerMock.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
rdvanbuuren
  • 596
  • 6
  • 13
  • 6
    I got an error because `cf.CreateClient()` is an extension method, which Moq doesn't allow in a mock expression. To fix the problem, use `clientFactoryMock.Setup(cf => cf.CreateClient(It.IsAny()))`. – Isaac Lyman May 13 '22 at 16:06
  • @IsaacLyman try mocking `.CreateClient(It.IsAny())` – sourcx Dec 05 '22 at 10:10
13

I was using the example from @Nkosi but with .NET 5 I got the following warning with the package Microsoft.AspNet.WebApi.Core needed for HttpConfiguration.

Warning NU1701 Package 'Microsoft.AspNet.WebApi.Core 5.2.7' was restored using '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8' instead of the project target framework 'net5.0'. This package may not be fully compatible with your project.

Complete example without using HttpConfiguration:

private LoginController GetLoginController()
{
    var expected = "Hello world";
    var mockFactory = new Mock<IHttpClientFactory>();

    var mockMessageHandler = new Mock<HttpMessageHandler>();
    mockMessageHandler.Protected()
        .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(new HttpResponseMessage
        {
            StatusCode = HttpStatusCode.OK,
            Content = new StringContent(expected)
        });

    var httpClient = new HttpClient(mockMessageHandler.Object);

    mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);

    var logger = Mock.Of<ILogger<LoginController>>();

    var controller = new LoginController(logger, mockFactory.Object);

    return controller;
}

Source:

HttpConfiguration from System.Web.Http in .NET 5 project

Ogglas
  • 62,132
  • 37
  • 328
  • 418
7

For those looking to achieve the same result of utilising a mock IHttpClientFactory with the HttpClient delegate to avoid making calls to endpoints during testing and who are using a version of .NET Core higher than 2.2 (where it seems the Microsoft.AspNet.WebApi.Core package containing the HttpRequestMessageExtensions.CreateResponse extension is no longer available without relying upon the package targeting the .NET Core 2.2) then the below adaption of Nkosi's answer above has worked for me in .NET 5.

One can simply use an instance of HttpRequestMessage directly if that is all that is required.

public class HttpHandlerStubDelegate : DelegatingHandler
{
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handlerFunc;
    
    public HttpHandlerStubDelegate()
    {
        _handlerFunc = (request, cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
    }

    public HttpHandlerStubDelegate(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handlerFunc)
    {
        _handlerFunc = handlerFunc;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return _handlerFunc(request, cancellationToken);
    }
}

As for the usage in the test Setup method, similarly, I've used an instance of HttpResponseMessage directly. In my case, the factoryMock is then passed into a custom Adapter which wraps around the HttpClient and is therefore set to use our fake HttpClient.

var expected = @"{ ""foo"": ""bar"" }";
var clientHandlerStub = new HttpHandlerStubDelegate((request, cancellationToken) => {
    var response = new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent(expected) };
    return Task.FromResult(response);
});

var factoryMock = new Mock<IHttpClientFactory>();
factoryMock.Setup(m => m.CreateClient(It.IsAny<string>()))
    .Returns(() => new HttpClient(clientHandlerStub));

And finally, an example NUnit test body using this which passes.

[Test]
public async Task Subject_Condition_Expectation()
{
    var expected = @"{ ""foo"": ""bar"" }";

    var result = await _myHttpClientWrapper.GetAsync("https://www.example.com/api/stuff");
    var actual = await result.Content.ReadAsStringAsync();

    Assert.AreEqual(expected, actual);
}
Cavyn VonDeylen
  • 4,189
  • 9
  • 37
  • 52
cognophile
  • 802
  • 11
  • 25
2

This code threw this exception for me, System.InvalidOperationException: The request does not have an associated configuration object or the provided configuration was null.

So included this in the test method, and it works.

var configuration = new HttpConfiguration();
var request = new HttpRequestMessage();
request.SetConfiguration(configuration);
Harish S
  • 69
  • 1
  • 11
0

A different approach may be to create an extra class that will internally call the service. This class can be mocked easily. It is not a direct answer to the question, but it seems a lot less complex and more testable.

Gaston
  • 49
  • 5
0

For what it's worth, this is my implementation using .NET 7 and Azure Functions v4. It is a workable, multi-request capable HttpClientFactory mock.

Unit Test Mock Setup


MockHttpClientFactory

public class MockHttpClientFactory
{
    public static IHttpClientFactory Create(string name, MockHttpResponse response)
    {
        return Create(name, new List<MockHttpResponse> { response });
    }


    public static IHttpClientFactory Create(string name, List<MockHttpResponse> responses)
    {
                    
        Mock<HttpMessageHandler> messageHandler = SendAsyncHandler(responses);

        var mockHttpClientFactory = new Mock<IHttpClientFactory>();

        mockHttpClientFactory
            .Setup(x => x.CreateClient(name))
            .Returns(new HttpClient(messageHandler.Object)
            {
                BaseAddress = new Uri("https://mockdomain.mock")
            });

        return mockHttpClientFactory.Object;
    }


    private static Mock<HttpMessageHandler> SendAsyncHandler(List<MockHttpResponse> responses)
    {
        var messageHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);

        foreach(var response in responses)
        {
            messageHandler
                .Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync",
                    ItExpr.Is<HttpRequestMessage>(r => r.RequestUri!.PathAndQuery == response.UrlPart),
                    ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = response.StatusCode,
                    Content = (response.Response?.GetType() == typeof(string)
                        ? new StringContent(response.Response?.ToString() ?? "")
                        : new StringContent(JsonSerializer.Serialize(response.Response)))
                })
                .Verifiable();
        }               

        return messageHandler;
    }
}

MockHttpResponse

public class MockHttpResponse
{
    public MockHttpResponse()
    {           
    }

    public MockHttpResponse(string urlPart, object response, HttpStatusCode statusCode)
    {
        this.UrlPart = urlPart;
        this.Response = response;
        this.StatusCode = statusCode;
    }


    public string UrlPart { get; set; } = String.Empty;

    public object Response { get; set; } = default!;

    public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
}

MockHttpRequestData

public class MockHttpRequestData
{ 
    public static HttpRequestData Create()
    {
        return Create<string>("");
    }   
    

    public static HttpRequestData Create<T>(T requestData) where T : class
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddFunctionsWorkerDefaults();

        var serializedData = JsonSerializer.Serialize(requestData);
        var bodyDataStream = new MemoryStream(Encoding.UTF8.GetBytes(serializedData));

        var context = new Mock<FunctionContext>();
        context.SetupProperty(context => context.InstanceServices, serviceCollection.BuildServiceProvider());

        var request = new Mock<HttpRequestData>(context.Object);
        request.Setup(r => r.Body).Returns(bodyDataStream);
        request.Setup(r => r.CreateResponse()).Returns(new MockHttpResponseData(context.Object));

        return request.Object;
    }
}

MockHttpResponseData

public class MockHttpResponseData : HttpResponseData
{
    public MockHttpResponseData(FunctionContext functionContext) : base(functionContext)
    {           
    }
    

    public override HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;

    public override HttpHeadersCollection Headers { get; set; } = new HttpHeadersCollection();

    public override Stream Body { get; set; } = new MemoryStream();

    public override HttpCookies Cookies { get; }
}

Usage


The Azure Function Method

This azure function has been setup with DI and uses an HttpClient object. Details are out of scope for this post. You can Google for more info.

public class Function1
{
    private readonly HttpClient httpClient;


    public Function1(IHttpClientFactory httpClientFactory)
    {
        this.httpClient = httpClientFactory.CreateClient("WhateverYouNamedIt");
    }



    [Function("Function1")]
    public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
    {
        var httpResponse = await this.httpClient.GetAsync("/some-path");
        var httpResponseContent = await httpResponse.Content.ReadAsStringAsync();

        // do something with the httpResponse or Content

        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteStringAsync(httpResponseContent);
        
        return response;
    }               
}

Simple Use Case

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        var httpClientFactory = MockHttpClientFactory.Create("WhateverYouNamedIt", new MockHttpResponse());

        var exception = Record.Exception(() => new Function1(httpClientFactory));

        Assert.Null(exception);
    }
}

More Realistic Use Case

    [Fact]
    public async Task Test2()
    {
        var httpResponses = new List<MockHttpResponse>
        {
            new MockHttpResponse
            {
                UrlPart = "/some-path",
                Response = new { Name = "data" }
            }
        };

        var httpClientFactory = MockHttpClientFactory.Create("WhateverYouNamedIt", httpResponses);
        var httpRequestData = MockHttpRequestData.Create();

        var function1 = new Function1(httpClientFactory);
        var function1Response = await function1.Run(httpRequestData);
        function1Response.Body.Position = 0;

        using var streamReader = new StreamReader(function1Response.Body);
        var function1ResponseBody = await streamReader.ReadToEndAsync();
                
        Assert.Equal("{\"Name\":\"data\"}", function1ResponseBody);
    }
BeanFlicker
  • 503
  • 1
  • 9
  • 23