A .NET Standard library for intercepting server-side HTTP dependencies.
Linux | Windows | |
---|---|---|
Build Status | ||
Build History |
This library provides functionality for intercepting HTTP requests made using the HttpClient
class in code targeting .NET Standard 1.3 and 2.0 (and later), and .NET Framework 4.6.1 and later.
The primary use-case is for providing stub responses for use in tests for applications, such as an ASP.NET Core application, to drive your functional test scenarios.
The library is based around an implementation of DelegatingHandler
, which can either be used directly as an implementation of HttpMessageHandler
, or can be provided to instances of HttpClient
. This also allows it to be registered via Dependency Injection to make it available for use in code under test without the application itself requiring any references to JustEat.HttpClientInterception
or any custom abstractions of HttpClient
.
This design means that no HTTP server needs to be hosted to proxy traffic to/from, so does not consume any additional system resources, such as needing to bind a port for HTTP traffic, making it lightweight to use.
To install the library from NuGet using the .NET SDK run:
dotnet add package JustEat.HttpClientInterception
Below is a minimal example of intercepting a request to an HTTP API for a JSON resource to return a custom response:
// using JustEat.HttpClientInterception;
var options = new HttpClientInterceptorOptions();
var builder = new HttpRequestInterceptionBuilder()
.Requests()
.ForHost("public.je-apis.com")
.ForPath("terms")
.Responds()
.WithJsonContent(new { Id = 1, Link = "https://www.just-eat.co.uk/privacy-policy" })
.RegisterWith(options);
var client = options.CreateHttpClient();
// The value of json will be "{\"Id\":1,\"Link\":\"https://www.just-eat.co.uk/privacy-policy\"}"
var json = await client.GetStringAsync("http://public.je-apis.com/terms");
HttpRequestInterceptionBuilder
objects are mutable, so properties can be freely changed once a particular setup has been registered with an instance of HttpClientInterceptorOptions
as the state is captured at the point of registration. This allows multiple responses and paths to be configured from a single HttpRequestInterceptionBuilder
instance where multiple registrations against a common hostname.
Below is a minimal example of intercepting a request to inject an HTTP fault:
// using JustEat.HttpClientInterception;
var options = new HttpClientInterceptorOptions();
var builder = new HttpRequestInterceptionBuilder()
.Requests()
.ForHost("public.je-apis.com")
.WithStatus(HttpStatusCode.InternalServerError)
.RegisterWith(options);
var client = options.CreateHttpClient();
// Throws an HttpRequestException
await client.GetStringAsync("http://public.je-apis.com");
If you are using IHttpClientFactory
to register HttpClient
for Dependency Injection in a .NET Core 2.1 application (or later), you can implement a custom IHttpMessageHandlerBuilderFilter
to register during test setup, which makes an instance of HttpClientInterceptorOptions
available to inject an HTTP handler.
A working example of this approach can be found in the sample application.
using Microsoft.Extensions.Http;
public class InterceptionFilter : IHttpMessageHandlerBuilderFilter
{
private readonly HttpClientInterceptorOptions _options;
internal InterceptionFilter(HttpClientInterceptorOptions options)
{
_options = options;
}
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
{
return (builder) =>
{
// Run any actions the application has configured for itself
next(builder);
// Add the interceptor as the last message handler
builder.AdditionalHandlers.Add(_options.CreateHttpMessageHandler());
};
}
}
var options = new HttpClientInterceptorOptions();
services.AddSingleton<IHttpMessageHandlerBuilderFilter, InterceptionFilter>(
(_) => new InterceptionFilter(options));
Below is an example of setting up IServiceCollection
to register HttpClient
for Dependency Injection in a manner that allows tests to use HttpClientInterceptorOptions
to intercept HTTP requests. Similar approaches can be used with other IoC containers.
You may wish to consider registering HttpClient
as a singleton, rather than as transient, if you do not use properties such as BaseAddress
on instances of HttpClient
. This allows the same instance to be used throughout the application, which improves performance and resource utilisation under heavy server load. If using a singleton instance, ensure that you manage the lifetime of your message handlers appropriately so they are not disposed of incorrectly and update the registration for your HttpClient
instance appropriately.
services.AddTransient(
(serviceProvider) =>
{
// Create a handler that makes actual HTTP calls
HttpMessageHandler handler = new HttpClientHandler();
// Have any delegating handlers been registered?
var handlers = serviceProvider
.GetServices<DelegatingHandler>()
.ToList();
if (handlers.Count > 0)
{
// Attach the initial handler to the first delegating handler
DelegatingHandler previous = handlers.First();
previous.InnerHandler = handler;
// Chain any remaining handlers to each other
foreach (DelegatingHandler next in handlers.Skip(1))
{
next.InnerHandler = previous;
previous = next;
}
// Replace the initial handler with the last delegating handler
handler = previous;
}
// Create the HttpClient using the inner HttpMessageHandler
return new HttpClient(handler);
});
Then in the test project register HttpClientInterceptorOptions
to provide an implementation of DelegatingHandler
. If using a singleton for HttpClient
as described above, update the registration for the tests appropriately so that the message handler is shared.
var options = new HttpClientInterceptorOptions();
var server = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureServices(
(services) => services.AddTransient((_) => options.CreateHttpMessageHandler()))
.Build();
server.Start();
Further examples of using the library can be found by following the links below:
- Example tests
- Sample application with tests
- This library's own tests
Generated with the Benchmarks project using BenchmarkDotNet using commit e189875 on 14/10/2018.
BenchmarkDotNet=v0.11.1, OS=Windows 10.0.17763
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=2.1.403
[Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
DefaultJob : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
Method | Mean | Error | StdDev | Median | Gen 0 | Allocated |
---|---|---|---|---|---|---|
GetBytes |
8.675 μs | 0.2513 μs | 0.7330 μs | 8.478 μs | 1.3275 | 4.13 KB |
GetHtml |
9.406 μs | 0.2480 μs | 0.7313 μs | 9.141 μs | 1.4191 | 4.41 KB |
GetJson |
17.653 μs | 0.3501 μs | 0.5849 μs | 17.428 μs | 3.0518 | 9.44 KB |
GetStream |
74.822 μs | 1.4916 μs | 3.7695 μs | 73.784 μs | 1.0986 | 3.43 KB |
Refit |
43.953 μs | 0.8722 μs | 0.9694 μs | 44.026 μs | 4.8218 | 14.97 KB |
Any feedback or issues can be added to the issues for this project in GitHub.
The repository is hosted in GitHub: https://github.com/justeat/httpclient-interception.git
Compiling the library yourself requires Git and the .NET Core SDK to be installed (version 2.1.500 or later).
To build and test the library locally from a terminal/command-line, run one of the following set of commands:
Linux/macOS
git clone https://github.com/justeat/httpclient-interception.git
cd httpclient-interception
./build.sh
Windows
git clone https://github.com/justeat/httpclient-interception.git
cd httpclient-interception
.\Build.ps1
This project is licensed under the Apache 2.0 license.