2

Update 20221024: I have used Ruikai Feng's solution in order to use Mockoon with my tests. I realize this is not a correct approach from a unit testing approach and am working to change my approach.

Update 20221019: I have been using moq to mock out the IHttpClientFactory. The reason why I wanted to instantiate it was to call mock apis created in a tool called Mockoon which replicates apis. I have been so far unable to call these APIs likely because I have not yet properly mocked the ihttpclientfactory. I appreciate all the feedback as the solution is still ongoing at this time.

I am using a .NET 6 Web API controller with IHttpClientFactory to perform external API calls. As such, I have the following constructor:

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

This works because in my Program.cs I add an HTTP Client to my builder.Services.

In my tests, how do I instantiate/set up the httpClientFactory for the controller because I need it to instantiate my controller: var controller = new MyController(httpClientFactory); generates an error since there isn't any settings added.

I ran into a similar issue with configurations from appsettings.json and resolved with ConfigurationBuilder but there doesn't seem to be a similar one for IHttpClientFactory.

If you need any more information, please let me know. Thanks!

4
  • Just out of curiosity why did you mark a solution proposal as the answer which is fundamentally wrong from unit testing perspective? Commented Oct 21, 2022 at 5:32
  • Hello Peter, I hope this following explanation is sound. I was using that solution to communicate with a mock API tool, Mockoon, that can be embedded in my test chain. Within the tool I can create mock API endpoints that replicate the same calls as the API I am testing with several cases. In my test cases, I use this method in my SetUp() and test against each endpoint testing one method and assert the specific values and parameters. The reason I am using Mockoon is to remove dependency on calling the actual API endpoint itself and call the mock one instead. Commented Oct 24, 2022 at 20:56
  • That's an integration test, where you mock your downstream http service(s). If you want to perform integration testing then crafting a ServiceCollection by hand is suboptimal. If you want to perform unit testing then you need to Mock the IHttpClientFactory and all related stuff, just as I described in my proposed solution. Commented Oct 25, 2022 at 7:38
  • 1
    I appreciate your help Peter! I have chosen your solution and will be working on implementing this mocking into my unit tests and use the previous effort/proposed solution for integration tests. Thanks again! Commented Oct 25, 2022 at 16:21

3 Answers 3

2

In order to be able to use a properly mocked IHttpClientFactory in your unit test you need to do the following steps:

Setup a DelegatingHandler mock

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

This sample mock will always return with 200 OK status code and without a response body

  • Tailor the setup for your needs

Create an HttpClient

var httpClient = new HttpClient(mockHandler.Object);

It creates an HttpClient instance and pass the above handler to it

Setup an IHttpClientFactory mock

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

It setups an IHttpClientFactory mock to return the above HttpClient for the CreateClient method call

  • If you use the IHttpClientFactory to create a named client then change the Setup to this .Setup(factory => factory.CreateClient(It.IsAny<string>()))

Use the mock objects for verification

mockFactory.Verify(factory => factory.CreateClient(), Times.Once); 
mockHandler.Protected()
   .Verify("SendAsync", Times.Once(), It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>());
Sign up to request clarification or add additional context in comments.

Comments

1

I'd like to amend @Peter CSala's answer some, since it got me most of the way there but not entirely. My service class uses the IHttpClientFactory but without any named client. When setting up the above code and running as is, I'm met with an exception:

Message: 
    System.NotSupportedException : Unsupported expression: factory => factory.CreateClient()
    Extension methods (here: HttpClientFactoryExtensions.CreateClient) may not be used in setup / verification expressions.

  Stack Trace: 
    Guard.IsOverridable(MethodInfo method, Expression expression) line 87
    ExpressionExtensions.<Split>g__Split|5_0(Expression e, Expression& r, MethodExpectation& p, Boolean assignment, Boolean allowNonOverridableLastProperty) line 234
    ExpressionExtensions.Split(LambdaExpression expression, Boolean allowNonOverridableLastProperty) line 149
    Mock.SetupRecursive[TSetup](Mock mock, LambdaExpression expression, Func`4 setupLast, Boolean allowNonOverridableLastProperty) line 643
    Mock.Setup(Mock mock, LambdaExpression expression, Condition condition) line 498
    Mock`1.Setup[TResult](Expression`1 expression) line 452
    MyServiceTests.GetHttpClientFactoryMock(Int64 contentLength) line 84
    MyServiceTests.DoesCopy_PutObject() line 55
    GenericAdapter`1.GetResult()
    AsyncToSyncAdapter.Await(Func`1 invoke)
    TestMethodCommand.RunTestMethod(TestExecutionContext context)
    TestMethodCommand.Execute(TestExecutionContext context)
    <>c__DisplayClass1_0.<Execute>b__0()
    DelegatingTestCommand.RunTestMethodInThreadAbortSafeZone(TestExecutionContext context, Action action)

This is because the parameter-less call to IHttpClientFactory.CreateClient() is actually an extension method, where the method call for a named client is not. Looking into the extension method, it actually is just a convenience wrapper for the named client method using a default name from Microsoft.Extensions.Options.

All you have to do is actually mock the named client method to return the mocked HttpClient from Peter's example above when requesting the default name. Here is the entire code I used to mock the IHttpClientFactory to work with a default client and workaround the above exception:

using Microsoft.Extensions.Options;

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

var httpClient = new HttpClient(mockHandler.Object);
var mockFactory = new Mock<IHttpClientFactory>(MockBehavior.Strict);
mockFactory
    .Setup(factory => factory.CreateClient(Options.DefaultName))
    .Returns(httpClient)
    .Verifiable();

// Use the factory from above in your tests
// Calls in the service to IHttpClientFactory.CreateClient() will
// return the HttpClient using the mocked handler from above :)
var res = myService.DoTheThing(mockFactory.Object);

Comments

0

I tried as below:

[TestFixture]
    public class IndexActionTests
    {
        private HomeController controller;

        [SetUp]
        public void Setup()
        {
            var services = new ServiceCollection();
            services.AddHttpClient();
            var provider = services.BuildServiceProvider();
            var httpclientfactory = provider.GetService<IHttpClientFactory>();
            controller = new HomeController(httpclientfactory);
        }

        [Test]
        public void Test1()
        {
            var result = controller.Index();
            Assert.AreEqual(typeof(ViewResult),result.GetType());
        }
    }

Result:

enter image description here

1 Comment

1. Create a DI container 2. Register the DefaultHttpClientFactory into it 3. Retrieve the factory to inject it via ctor. << You should not do these steps in a unit test. You should mock the dependencies not re-create them. With your setup the HomeController will issue a real HTTP request, which is not the intent of a unit test. It is more closer to an integration or e2e test.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.