usagibear
usagibear

Reputation: 395

Nunit 3: Test a Controller that uses IHttpClientFactory as Constructor Parameter

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!

Upvotes: 2

Views: 1998

Answers (3)

Tyler Roesler
Tyler Roesler

Reputation: 11

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);

Upvotes: 1

Peter Csala
Peter Csala

Reputation: 22679

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>());

Upvotes: 2

Ruikai Feng
Ruikai Feng

Reputation: 11546

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

Upvotes: 0

Related Questions