- Summary
- Motivation
- Requirements
- How to use
- Mock different types of HttpClients
- Mock responses conditionally
- Mock several responses
- Different ways to mock the HttpClient response
- Configure the http response mocks with access to the
IServiceProvider
This will allow mocking the HttpClient's response by taking control of the HttpMessageHandler(s) that is(are) used by the HttpClient(s) registered on the Progam.
The mocking of the http response happens within the test server, without any outgoing http call actually happening, and that's why this method was named in process as oposed to the out-of-process http response mocking method.
For more information see mocking HttpClient's responses using in-process vs out-of-process.
I want to be able to do integration tests as defined in introduction to integration tests and the scenario I want to test includes outgoing http calls made by the HttpClient
.
When doing these types of tests you need to be able to inject mock services. The docs explain how to do this. And when it comes to HttpClient calls it means you have two choices:
- Use a typed HttpClient and inject a mock for that type
- Mock the IHttpClientFactory
The problem with mocking the typed client is that then we won't test the implementation of the typed client. In this scenario we mock more than just the http layer. We mock the implementation of the typed client. In the end, in addition to integration tests, we would also have to implement some unit tests for the implementation of the typed client.
The problem with mocking the IHttpClientFactory.CreateClient
is that any configuration that is set for the HttpClient
as part of the IServiceCollection.AddHttpClient
won't take effect.
For instance, after calling IServiceCollection.AddHttpClient
you can configure properties/behaviour of the HttpClient
by following that call with a IHttpClientBuilder.ConfigureHttpClient
. Imagine that you want to configure a base address or a timeout for the HttpClient
. If we mock the IHttpClientFactory.CreateClient
then the call to IHttpClientBuilder.ConfigureHttpClient
where you define the base address or a timeout won't take effect during tests because we aren't using the 'real' IHttpClientFactory
.
As another example, if you use the Polly library to add resilience and transient-fault-handling to the HttpClient
then those policies will also not take effect on your tests leaving a gap in testing.
You will have to add the dotnet-sdk-extensions-testing nuget to your test project.
Start by creating an integration test as shown in introduction to integration tests.
After, configure the responses of the HttpClient by using the IWebHostBuilder.UseHttpMocks
extension method. See example DemoTest:
public class HttpMocksDemoTests : IClassFixture<WebApplicationFactory<Progam>>
{
private readonly WebApplicationFactory<Progam> _webApplicationFactory;
public HttpMocksDemoTests(WebApplicationFactory<Progam> webApplicationFactory)
{
_webApplicationFactory = webApplicationFactory;
}
[Fact]
public void DemoTest()
{
var httpClient = _webApplicationFactory
.WithWebHostBuilder(builder =>
{
builder
.ConfigureTestServices(services =>
{
// inject mocks for any other services
})
.UseHttpMocks(handlers =>
{
handlers.MockHttpResponse(httpResponseMessageBuilder =>
{
httpResponseMessageBuilder
.ForTypedClient<IMyApiClient>()
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.OK);
});
});
});
})
.CreateClient();
// do some calls to your app via the httpClient and then some asserts
}
}
[!NOTE]: the above test assumes that there is a typed client, represented by the type IMyApiClient
, added to the IServiceCollection
of the Progam
class through the IServiceCollection.AddHttpClient
method.
There are 3 ways to create http clients using IHttpClientFactory.
When mocking the http responses, the way you create the mock response varies depending on the type of HttpClient registered.
For typed clients you need to provide the type of the client when using HttpResponseMessageMockDescriptorBuilder.ForTypedClient
:
var httpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
httpResponseMock
.ForTypedClient<IMyApiClient>()
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
For named clients you need to provide the name of the client when using HttpResponseMessageMockDescriptorBuilder.ForNamedClient
:
var httpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
httpResponseMock
.ForNamedClient("ClientName")
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
For http clients created following the Basic usage of the IHttpClientFactory
use the HttpResponseMessageMockDescriptorBuilder.ForBasicClient
:
var httpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
httpResponseMock
.ForBasicClient()
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
You can mock responses conditially by using the InProcessHttpResponseMessageMockBuilder.Where
method.
Imagine that you have a typed client which implemented 3 different API calls but you only wanted to mock the response for one of them. You can do:
var httpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
httpResponseMock
.ForTypedClient<IMyApiClient>()
.Where(HttpRequestMessage =>
{
return HttpRequestMessage.RequestUri.AbsolutePath.Equals("/Users");
})
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
The above will mock http responses to the IMyApiClient
typed HttpClient
when the request path is /Users
.
By default, if InProcessHttpResponseMessageMockBuilder.Where
method is not used, it will always apply the mock.
If multiple http response mocks implement the same condition then only the response from the first mock is returned.
If you create a mock for an HttpClient
and no condition is met you will receive an InvalidOperationException
indicating which endpoint is being called but not mocked.
You can mock multiple http responses:
var usersHttpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
usersHttpResponseMock
.ForTypedClient<IMyApiClient>()
.Where(HttpRequestMessage =>
{
return HttpRequestMessage.RequestUri.AbsolutePath.Equals("/Users");
})
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
var adminsHttpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
adminsHttpResponseMock
.ForTypedClient<IMyApiClient>()
.Where(HttpRequestMessage =>
{
return HttpRequestMessage.RequestUri.AbsolutePath.Equals("/Admins");
})
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
and then feed the mocks to the IWebHostBuilder.UseHttpMocks
extension method:
UseHttpMocks(handlers =>
{
handlers
.MockHttpResponse(usersHttpResponseMock)
.MockHttpResponse(adminsHttpResponseMock);
});
You might have noticed that the last example of mocking the http response is differen from the first one show in How to use. In short, you can chose to define the mocks inline or before hand.
There is no recommendation on any of the different ways to do the mocking. You should use the option that better fits your scenario/style.
Let's see some examples:
- Configuring the http response mocks inline with the
HttpMessageHandlersReplacer.MockHttpResponse
method:
public class HttpMocksDemoTests : IClassFixture<WebApplicationFactory<Progam>>
{
private readonly WebApplicationFactory<Progam> _webApplicationFactory;
public HttpMocksDemoTests(WebApplicationFactory<Progam> webApplicationFactory)
{
_webApplicationFactory = webApplicationFactory;
}
[Fact]
public void DemoTest()
{
var httpClient = _webApplicationFactory
.WithWebHostBuilder(builder =>
{
builder
.ConfigureTestServices(services =>
{
// inject mocks for any other services
})
.UseHttpMocks(handlers =>
{
handlers.MockHttpResponse(httpResponseMessageBuilder =>
{
httpResponseMessageBuilder
.ForTypedClient<IMyApiClient>()
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
});
});
}).CreateClient();
// do some calls to your app via the httpClient and then some asserts
}
}
- Configuring the http response mocks before hand and using them with
IWebHostBuilder.UseHttpMocks
inline:
public class HttpMocksDemoTests : IClassFixture<WebApplicationFactory<Progam>>
{
private readonly WebApplicationFactory<Progam> _webApplicationFactory;
public HttpMocksDemoTests(WebApplicationFactory<Progam> webApplicationFactory)
{
_webApplicationFactory = webApplicationFactory;
}
[Fact]
public void DemoTest()
{
var usersHttpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
usersHttpResponseMock
.ForTypedClient<IMyApiClient>()
.Where(HttpRequestMessage =>
{
return HttpRequestMessage.RequestUri.AbsolutePath.Equals("/Users");
})
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
var adminsHttpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
adminsHttpResponseMock
.ForTypedClient<IMyApiClient>()
.Where(HttpRequestMessage =>
{
return HttpRequestMessage.RequestUri.AbsolutePath.Equals("/Admins");
})
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
var httpClient = _webApplicationFactory
.WithWebHostBuilder(builder =>
{
builder
.ConfigureTestServices(services =>
{
// inject mocks for any other services
})
.UseHttpMocks(handlers =>
{
handlers.MockHttpResponse(usersHttpResponseMock);
handlers.MockHttpResponse(adminsHttpResponseMock);
});
}).CreateClient();
// do some calls to your app via the httpClient and then some asserts
}
}
- Configuring the http response mocks before hand and using them with
IWebHostBuilder.UseHttpMocks
non inline:
public class HttpMocksDemoTests : IClassFixture<WebApplicationFactory<Progam>>
{
private readonly WebApplicationFactory<Progam> _webApplicationFactory;
public HttpMocksDemoTests(WebApplicationFactory<Progam> webApplicationFactory)
{
_webApplicationFactory = webApplicationFactory;
}
[Fact]
public void DemoTest()
{
var usersHttpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
usersHttpResponseMock
.ForTypedClient<IMyApiClient>()
.Where(HttpRequestMessage =>
{
return HttpRequestMessage.RequestUri.AbsolutePath.Equals("/Users");
})
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
var adminsHttpResponseMock = new HttpResponseMessageMockDescriptorBuilder();
adminsHttpResponseMock
.ForTypedClient<IMyApiClient>()
.Where(HttpRequestMessage =>
{
return HttpRequestMessage.RequestUri.AbsolutePath.Equals("/Admins");
})
.RespondWith(httpRequestMessage =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
});
var httpClient = _webApplicationFactory
.WithWebHostBuilder(builder =>
{
builder
.ConfigureTestServices(services =>
{
// inject mocks for any other services
})
.UseHttpMocks(usersHttpResponseMock,adminsHttpResponseMock);
}).CreateClient();
// do some calls to your app via the httpClient and then some asserts
}
}
If you need to configure the http response mock based on data that depends on what is present on the IServiceCollection
then you can use the overload that gives you access to the IServiceProvider
to retrieve what you require. For instance:
public class HttpMocksDemoTests : IClassFixture<WebApplicationFactory<Progam>>
{
private readonly WebApplicationFactory<Progam> _webApplicationFactory;
public HttpMocksDemoTests(WebApplicationFactory<Progam> webApplicationFactory)
{
_webApplicationFactory = webApplicationFactory;
}
[Fact]
public void DemoTest()
{
var httpClient = _webApplicationFactory
.WithWebHostBuilder(builder =>
{
builder.UseSetting("SomeOption", "my-option-value");
builder
.ConfigureTestServices(services =>
{
// inject mocks for any other services
})
.UseHttpMocks(handlers =>
{
handlers.MockHttpResponse((serviceProvider, httpResponseMessageBuilder) =>
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var valueFromConfiguration = configuration.GetValue<string>("SomeOption");
httpResponseMessageBuilder
.ForTypedClient<IMyApiClient>()
.RespondWith(httpRequestMessage =>
{
var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK);
httpResponseMessage.Headers.Add("some-header", valueFromConfiguration);
return httpResponseMessage;
});
});
});
}).CreateClient();
// do some calls to your app via the httpClient and then some asserts
}
}
In the above example we are retrieving the configuration value for the key SomeOption
from the IConfiguration
instance that we got from the IServiceProvider
and setting it as the value of the header some-header
for the mocked response.