Dr Schizo
Dr Schizo

Reputation: 4376

Unit testing DelegatingHandler

How do I unit test a custom DelegatingHandler? I have the following but its complaining the innerHandler not set.

var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://foo.com");
var handler = new FooHandler()
{
    InnerHandler = new FooHandler() 
};

var invoker = new HttpMessageInvoker(handler);
var result = await invoker.SendAsync(httpRequestMessage, new CancellationToken());

Assert.That(result.Headers.GetValues("some-header").First(), Is.Not.Empty, "");

Upvotes: 32

Views: 14408

Answers (4)

Ray
Ray

Reputation: 192406

You can set the InnerHandler property of the DelegatingHandler you're testing (FooHandler) with a dummy/fake handler (TestHandler) as shown in that linked post in your comment.

public class TestHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
    }
}

 // in your test class method
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://example.com/");
    var handler = new FooHandler()
    {
        InnerHandler = new TestHandler()  // <-- change to use this

    };

    var invoker = new HttpMessageInvoker(handler);
    var result = await invoker.SendAsync(httpRequestMessage, new CancellationToken());

    Assert.That(result.Headers.GetValues("some-header").First(), Is.Not.Empty, "");

Unlike that post, this should be the minimum you need to set up to get your test to run.

Upvotes: 44

hIpPy
hIpPy

Reputation: 5125

You just need to wire FooHandler's InnerHandler to an instance of type HttpMessageHandler.

You can mock using AutoFixture as fixture.Create<HttpMessageHandler>(), or you can handle-roll your own, or you can use the awesome MockHttpMessageHandler.

TLDR

There are a few small things off with the answers here so I thought of adding my answer. I recall seeing similar code as mentioned in Ray's answer, and I was thrown off by the usage of Task.Factory.StartNew() in there, which is totally unnecessary. Moq.Protected is a bit brittle for my taste; it's ok in this situation as "SendAsync" method won't change mostly.

The Delegating Handler follows the chain-of-responsibility pattern where one can form a chain. One doesn't get to control the last handler which will be HttpMessageHandler, an abstract class.

So, the simplest form of chain is,

HttpClient -> HttpMessageHandler impl

In your case, the chain is,

HttpMessageInvoker -> FooHandler -> HttpMessageHandler impl

It's convenient to use the plumbing class HttpMessageInvoker for testing, instead of the porcelain class HttpClient. In your example, HttpMessageInvoker ctor handles the part of wiring up its InnerHandler with FooHandler, but you need to wire up FooHandler's InnerHandler. Note that one doesn't need to do this for HttpClient as there exists a helper method, HttpClientFactory.Create() which handles the wiring. But there isn't an HttpMessageInvokerFactory.Create() method. I have few tests in my repo where I have scenarios with few handlers in the chain, and I eventually ended up writing that helper method (pasted below).

Note that only HttpMessageInvokerFactory handles the wiring part; with others, one has to do it explicitly.

Examples

Putting it all together, let's say one has a simple Delegating Handler to test as RelayHandler which simply relays the http request,

    /// <summary>
    /// Simply relays request, just for fun.
    /// </summary>
    public class RelayHandler : DelegatingHandler
    {
        /// <inheritdoc cref="RelayHandler" />
        public RelayHandler()
        { }

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

source: https://github.com/rmandvikar/delegating-handlers/blob/main/src/rm.DelegatingHandlers/RelayHandler.cs

One can test it as,

        [Test]
        public async Task Relays()
        {
            var fixture = new Fixture().Customize(new AutoMoqCustomization());

            var relayHandler = new RelayHandler();

            using var invoker = HttpMessageInvokerFactory.Create(
                fixture.Create<HttpMessageHandler>(), relayHandler);

            using var requestMessage = fixture.Create<HttpRequestMessage>();
            using var response = await invoker.SendAsync(requestMessage, CancellationToken.None);

            Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        }

source: https://github.com/rmandvikar/delegating-handlers/blob/main/tests/rm.DelegatingHandlersTest/RelayHandlerTests.cs

where HttpMessageInvokerFactory is as,

    public static class HttpMessageInvokerFactory
    {
        public static HttpMessageInvoker Create(
            params DelegatingHandler[] handlers)
        {
            return Create(null!, handlers);
        }

        public static HttpMessageInvoker Create(
            HttpMessageHandler innerHandler,
            params DelegatingHandler[] handlers)
        {
            if (handlers == null || !handlers.Any())
            {
                throw new ArgumentNullException(nameof(handlers));
            }
            if (handlers.Any(x => x == null))
            {
                throw new ArgumentNullException(nameof(handlers), "At least one of the handlers is null.");
            }

            var first = handlers[0];

            Array.Reverse(handlers);
            var current = innerHandler;
            foreach (var next in handlers)
            {
                if (current != null)
                {
                    next.InnerHandler = current;
                }
                current = next;
            }
            return new HttpMessageInvoker(first);
        }
    }

source: https://github.com/rmandvikar/delegating-handlers/blob/main/tests/rm.DelegatingHandlersTest/misc/HttpMessageInvokerFactory.cs

The reason why FooHandler's InnerHandler needs to be setup is because its InnerHandler is executed is my guess in your test. That said, one could have a Delegating Handler that "short-circuits" the call, and one doesn't need its InnerHandler.

Here is a Delegating Handler, ThrowingHandler that throws unconditionally.

    /// <summary>
    /// Throws an exception.
    /// </summary>
    public class ThrowingHandler : DelegatingHandler
    {
        private readonly Exception exception;

        /// <inheritdoc cref="ThrowingHandler" />
        public ThrowingHandler(
            Exception exception)
        {
            // funny, no?
            this.exception = exception
                ?? throw new ArgumentNullException(nameof(exception));
        }

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

source: https://github.com/rmandvikar/delegating-handlers/blob/main/src/rm.DelegatingHandlers/ThrowingHandler.cs

I don't need to setup its InnerHandler in test as it's not used. :shrug: One could if they feel like to have a consistent test setup.

        [Test]
        public void Throws()
        {
            var fixture = new Fixture().Customize(new AutoMoqCustomization());

            var throwingHandler = new ThrowingHandler(new TurnDownForWhatException());

            using var invoker = HttpMessageInvokerFactory.Create(
                throwingHandler);

            using var requestMessage = fixture.Create<HttpRequestMessage>();
            var ex = Assert.ThrowsAsync<TurnDownForWhatException>(async () =>
            {
                using var _ = await invoker.SendAsync(requestMessage, CancellationToken.None);
            });
        }

// TurnDownForWhatException is a custom exception

source: https://github.com/rmandvikar/delegating-handlers/blob/main/tests/rm.DelegatingHandlersTest/ThrowingHandlerTests.cs

Note that the TestHandler in Ray's answer is better named as HttpStatusOkHandler. But, one could need a handler to spoof an Http 400 instead, and so on.

    public class HttpStatusOkHandler : DelegatingHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
        }
    }

So, eventually I came up with below.

    /// <summary>
    /// Short-circuits with a canned http response.
    /// </summary>
    /// <remarks>
    /// Canned response should not be disposed if used with a retry handler
    /// as it's meant for multiuse. If so, consider using <see cref="ShortCircuitingResponseHandler"/>
    /// instead.
    /// </remarks>
    public class ShortCircuitingCannedResponseHandler : DelegatingHandler
    {
        private readonly HttpResponseMessage response;

        /// <inheritdoc cref="ShortCircuitingCannedResponseHandler" />
        public ShortCircuitingCannedResponseHandler(
            HttpResponseMessage response)
        {
            this.response = response
                ?? throw new ArgumentNullException(nameof(response));
        }

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

source: https://github.com/rmandvikar/delegating-handlers/blob/main/src/rm.DelegatingHandlers/ShortCircuitingCannedResponseHandler.cs

Upvotes: 1

rdvanbuuren
rdvanbuuren

Reputation: 706

With using Moq.Protected;, you can get more control over the response outcome.

var request = new HttpRequestMessage();

var innerHandlerMock = new Mock<DelegatingHandler>(MockBehavior.Strict);
innerHandlerMock
  .Protected()
  .Setup<Task<HttpResponseMessage>>("SendAsync", request, ItExpr.IsAny<CancellationToken>())
  .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

var handler = new FooHandler()
{
  InnerHandler = innerHandlerMock.Object
};
var invoker = new HttpMessageInvoker(handler);

// act
await invoker.SendAsync(request, default);

Upvotes: 9

Andriy Tolstoy
Andriy Tolstoy

Reputation: 6100

Use e.g. System.Net.Http.HttpClientHandler or System.Web.Http.HttpServer as an InnerHandler:

var handler = new FooHandler()
{
    InnerHandler = new System.Net.Http.HttpClientHandler()
};

or

var handler = new FooHandler()
{
    InnerHandler = new System.Web.Http.HttpServer() 
};

Upvotes: 0

Related Questions