单元测试DelegatingHandler
本文关键字:DelegatingHandler 单元测试 | 更新日期: 2023-09-27 18:04:46
如何对自定义DelegatingHandler进行单元测试?我有以下,但它抱怨的innerHandler没有设置。
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, "");
您可以设置您正在测试的DelegatingHandler的InnerHandler
属性(FooHandler
)与虚拟/假处理程序(TestHandler
),如您评论中的链接帖子所示。
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, "");
与那篇文章不同,这应该是您需要设置的最小值,以使您的测试运行
使用using Moq.Protected;
,您可以更好地控制响应结果。
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);
使用System.Net.Http.HttpClientHandler或System.Web.Http.HttpServer作为InnerHandler:
var handler = new FooHandler()
{
InnerHandler = new System.Net.Http.HttpClientHandler()
};
或
var handler = new FooHandler()
{
InnerHandler = new System.Web.Http.HttpServer()
};
您只需要将FooHandler
的InnerHandler
连接到类型为HttpMessageHandler
的实例。
您可以模拟使用AutoFixture
作为fixture.Create<HttpMessageHandler>()
,或者您可以处理您自己的滚动,或者您可以使用令人敬畏的MockHttpMessageHandler
。
TLDR
这里的答案有一些小的东西,所以我想加上我的答案。我记得在Ray的回答中看到过类似的代码,我被Task.Factory.StartNew()
的使用所迷惑,这是完全没有必要的。Moq.Protected
对我的口味来说有点脆;在这种情况下,"SendAsync"
方法不会改变。
委托处理程序遵循责任链模式,其中可以形成链。不能控制最后一个处理程序,它将是HttpMessageHandler
,一个抽象类。
所以,链的最简单形式是
HttpClient -> HttpMessageHandler impl
在你的例子中,链是,
HttpMessageInvoker -> FooHandler -> HttpMessageHandler impl
用管道类HttpMessageInvoker
代替陶瓷类HttpClient
进行测试比较方便。在您的示例中,HttpMessageInvoker
ctor处理将InnerHandler
与FooHandler
连接起来的部分,但您需要将FooHandler
的InnerHandler
连接起来。注意,对于HttpClient
不需要这样做,因为存在一个助手方法HttpClientFactory.Create()
来处理连接。但是没有HttpMessageInvokerFactory.Create()
方法。在我的repo中,我有几个测试,在这些测试中,链中有几个处理程序,我最终编写了那个helper方法(粘贴在下面)。
注意,只有HttpMessageInvokerFactory
处理布线部分;对于其他人,你必须明确地去做。
把这一切放在一起,让我们说一个简单的委派处理程序测试作为RelayHandler
,它只是中继http请求,
/// <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);
}
}
来源:https://github.com/rmandvikar/delegating-handlers/blob/main/src/rm.DelegatingHandlers/RelayHandler.cs
可以这样测试
[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);
}
来源:https://github.com/rmandvikar/delegating-handlers/blob/main/tests/rm.DelegatingHandlersTest/RelayHandlerTests.cs
其中HttpMessageInvokerFactory
为,
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);
}
}
来源:https://github.com/rmandvikar/delegating-handlers/blob/main/tests/rm.DelegatingHandlersTest/misc/HttpMessageInvokerFactory.cs
需要设置FooHandler
的InnerHandler
的原因是因为它的InnerHandler
被执行了是我在你的测试中猜测的。也就是说,可以有一个"短路"的委托处理程序。调用,一个不需要它的InnerHandler
.
这是一个无条件抛出的委托处理程序ThrowingHandler
。
/// <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;
}
}
来源:https://github.com/rmandvikar/delegating-handlers/blob/main/src/rm.DelegatingHandlers/ThrowingHandler.cs
我不需要在测试中设置它的InnerHandler
,因为它没有使用。耸耸肩:如果他们想有一个一致的测试设置,可以。
[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
来源:https://github.com/rmandvikar/delegating-handlers/blob/main/tests/rm.DelegatingHandlersTest/ThrowingHandlerTests.cs
请注意,Ray回答中的TestHandler
最好被命名为HttpStatusOkHandler
。但是,可能需要一个处理程序来欺骗Http 400,等等。
public class HttpStatusOkHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
}
所以,最后我想出了下面这个。
/// <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);
}
}
来源:https://github.com/rmandvikar/delegating-handlers/blob/main/src/rm.DelegatingHandlers/ShortCircuitingCannedResponseHandler.cs