Mock http client that is set up inside of Program.cs - c#

I have a typed HttpClient that I am injecting in my application using the HttpClientFactory extension methods for services in Program.cs
My client looks something like this with the HttpClient injected via the constructor:
public class MyClient
{
private readonly HttpClient _httpClient;
public MyClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetStuffFromApi()
{
// method to get content from API
// return stuff
}
}
The relevant section in Program.cs looks something like this for example:
services.AddHttpClient<IMyClient, MyClient>(client =>
{
client.BaseAddress = new Uri("https://somewebsite.com/api");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
}).AddPolicyHandler(MyClientPolicies.GetRetryAsyncPolicy());
I would like to test the retry policy among other things for this client. I found a great mockhttp library that is helpful for the mock client setup (MockHttp), but I am unsure of exactly what I need to do in order to include the retry policy behavior in my mocked client.
So the test looks something like this using XUnit currently:
public class MyClientTests
{
[Fact]
public async Task MyClient_RetriesRequest_OnTransientErrors()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("*").Respond(HttpStatusCode.RequestTimeout);
var mockClient = new MyClient(mockHttp.ToHttpClient());
// Act
// ... call the method
// Assert
// ... assert the request was tried multiple times
}
}
How do I test my mock http client including the additional configuration from Program.cs like the baseaddress and retry policies?

You cannot test the retry policy if it's setup like that, in a simple unit test. You have 2 choices.
To create full-service integration tests and then get creative with mock services, following this guideline for integration tests from Microsoft: https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-7.0#inject-mock-services.
2.To use the retry policy directly in your method which you are testing. Something like:
public async Task<string> GetStuffFromApi()
{
var policy = MyClientPolicies.GetRetryAsyncPolicy()
await policy.ExecuteAsync(async ctx =>
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://www.example.com"));
var response = await _client.SendAsync(request);
response.EnsureSuccessStatusCode();
return response;
});
}

Related

How to do integration testing on an external API with ASP.NET Core

I'm trying to do some integration tests on an external API. Most of the guides I find online are about testing the ASP.NET web api, but there's not much to find about external API's. I want to test a GET request on this API and confirm if it passes by checking if the status code is OK. However this test is not passing and im wondering if i'm doing this correctly. Currently it's giving me a status code 404(Not found).
I'm using xUnit together with Microsoft.AspNetCore.TestHost How would you suggest me to test external API's?
private readonly HttpClient _client;
public DevicesApiTests()
{
var server = new TestServer(new WebHostBuilder()
.UseEnvironment("Development")
.UseStartup<Startup>());
_client = server.CreateClient();
}
[Theory]
[InlineData("GET")]
public async Task GetAllDevicesFromPRTG(string method)
{
//Arrange
var request = new HttpRequestMessage(new HttpMethod(method), "https://prtg.nl/api/content=Group,Device,Status");
//Act
var response = await _client.SendAsync(request);
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Edit
The API call which im trying to test looks as follows, and is working properly
private readonly DbContext _dbContext;
private readonly IDevicesRepository _devicesRepository;
public DevicesAPIController(DbContext dbContext, IDevicesRepository devicesRepository)
{
_dbContext = dbContext;
_devicesRepository = devicesRepository;
}
[HttpPost("PostLiveDevicesToDatabase")]
public async Task<IActionResult> PostLiveDevicesToDatabase()
{
try
{
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
using (var response = await httpClient
.GetAsync(
"https://prtg.nl/api/content=Group,Device,Status")
)
{
string apiResponse = await response.Content.ReadAsStringAsync();
var dataDeserialized = JsonConvert.DeserializeObject<Devices>(apiResponse);
devicesList.AddRange(dataDeserialized.devices);
foreach (DevicesData device in devicesList)
{
_dbContext.Devices.Add(device);
devicesAdded.Add(device);
_dbContext.SaveChanges();
}
}
}
}
catch
{
return BadRequest();
}
}
I would like to propose an alternative solution which involves changing the design of the code to be tested.
The currently shown test-case is coupled to the external API and tests its ability to respond 200 OK rather than your code (i.e., your code isn't referenced at all). This also means that if a connection can't be established to the server (e.g., could be an isolated build agent in a CI/CD pipeline or just a flaky café WIFI) the test fails for another reason than what is asserted.
I would propose to extract the HttpClient, and its configuration that is specific to the API, into an abstraction as you have done with the IDevicesRepository (although it's not used in the example). This allows you to substitute the response from the API and only test your code. The substitutions could explore edge-cases such as the connection down, empty response, malformed response, external server error etc. That way you can exercise more failure-paths in your code and keep the test decoupled from the external API.
The actual substitution of the abstraction would be done in the "arrange" phase of the test. You can use the Moq NuGet package for this.
Update
To provide an example of using Moq to simulate an empty API response consider a hypothetical abstraction such as:
public interface IDeviceLoader
{
public IEnumerable<DeviceDto> Get();
}
public class DeviceDto
{
// Properties here...
}
Keep in mind the example abstraction isn't asynchronous, which could be considered best practices as you are invoking I/O (i.e., the network). I skipped it to keep it simple. See Moq documentation on how to handle async methods.
To mock the response the body of the test case could be:
[Fact]
public async Task CheckEndpointHandlesEmptyApiResponse()
{
// How you get access to the database context and device repository is up to you.
var dbContext = ...
var deviceRepository = ...
//Arrange
var apiMock = new Mock<IDeviceLoader>();
apiMock.Setup(loader => loader.Get()).Returns(Enumerable.Empty<DeviceDto>());
var controller = new DevicesAPIController(dbContext, deviceRepository, apiMock.Object);
//Act
var actionResponse = controller.PostLiveDevicesToDatabase();
// Assert
// Check the expected HTTP result here...
}
Do check the Moq documentation on their repository (linked above) for more examples.
The base address of test server is localhost. TestServer is meant for in-memory integration tests. The client created via TestServer.CreateClient() will create an instance of HttpClient that uses an internal message handler to manage requests specific you your API.
If you are trying to access an external URL by calling the test server. You will get 404 by design.
If https://prtg.nl/api/content is not local to your API and is the actual external link you want to access then use an independent HttpClient
//...
private static readonly HttpClient _client;
static DevicesApiTests() {
_client = new HttpClient();
}
[Theory]
[InlineData("GET")]
public async Task GetAllDevicesFromPRTG(string method) {
//Arrange
var request = new HttpRequestMessage(new HttpMethod(method), "https://prtg.nl/api/content=Group,Device,Status");
//Act
var response = await _client.SendAsync(request);
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
//...
If this is meant to be an end to end via your api then you need to call the local API end point which is dependent on the target controller and action
The example in accepted solution is not an integration test, it's unit test. While it's usable in simple scenarios, I wouldn't recommend you to test controllers directly. On integration test level, controller is an implementation detail of your application. Testing implementation details is considered a bad practice. It makes your tests more flaky and less maintainable.
Instead, you should test your API directly using WebApplicationFactory from Microsoft.AspNetCore.Mvc.Testing package.
https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests
Here is how I would do it
Implementation
Add typed client wrapper around HttpClient
public class DeviceItemDto
{
// some fields
}
public interface IDevicesClient
{
Task<DeviceItemDto[]?> GetDevicesAsync(CancellationToken cancellationToken);
}
public class DevicesClient : IDevicesClient
{
private readonly HttpClient _client;
public DevicesClient(HttpClient client)
{
_client = client;
}
public Task<DeviceItemDto[]?> GetDevicesAsync(CancellationToken cancellationToken)
{
return _client.GetFromJsonAsync<DeviceItemDto[]>("/api/content=Group,Device,Status", cancellationToken);
}
}
Register your typed client in DI
public static class DependencyInjectionExtensions
{
public static IHttpClientBuilder AddDevicesClient(this IServiceCollection services)
{
return services.AddHttpClient<IDevicesClient, DevicesClient>(client =>
{
client.BaseAddress = new Uri("https://prtg.nl");
});
}
}
// Use it in Startup.cs
services.AddDevicesClient();
Use typed client in your controller
private readonly IDevicesClient _devicesClient;
public DevicesController(IDevicesClient devicesClient)
{
_devicesClient = devicesClient;
}
[HttpGet("save")]
public async Task<IActionResult> PostLiveDevicesToDatabase(CancellationToken cancellationToken)
{
var devices = await _devicesClient.GetDevicesAsync(cancellationToken);
// save to database code
// you can return saved devices, or their ids
return Ok(devices);
}
Tests
Add fake HttpMessageHandler for mocking HTTP responses
public class FakeHttpMessageHandler : HttpMessageHandler
{
private HttpStatusCode _statusCode = HttpStatusCode.NotFound;
private HttpContent? _responseContent;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(_statusCode)
{
Content = _responseContent
};
return Task.FromResult(response);
}
public FakeHttpMessageHandler WithDevicesResponse(IEnumerable<DeviceItemDto> devices)
{
_statusCode = HttpStatusCode.OK;
_responseContent = new StringContent(JsonSerializer.Serialize(devices));
return this;
}
}
Add custom WebApplicationFactory
internal class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
// Use the same method as in implementation
services.AddDevicesClient()
// Replaces the default handler with mocked one to avoid calling real API in tests
.ConfigurePrimaryHttpMessageHandler(() => new FakeHttpMessageHandler());
});
}
// Use this method in your tests to setup specific responses
public WebApplicationFactory<Program> UseFakeDevicesClient(
Func<FakeHttpMessageHandler, FakeHttpMessageHandler> configureHandler)
{
var handler = configureHandler.Invoke(new FakeHttpMessageHandler());
return WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddDevicesClient().ConfigurePrimaryHttpMessageHandler(() => handler);
});
});
}
}
Test will look like this:
public class GetDevicesTests
{
private readonly CustomWebApplicationFactory _factory = new();
[Fact]
public async void Saves_all_devices_from_external_resource()
{
var devicesFromExternalResource => new[]
{
// setup some test data
}
var client = _factory
.UseFakeDevicesClient(_ => _.WithDevicesResponse(devicesFromExternalResource))
.CreateClient();
var response = await client.PostAsync("/devices/save", CancellationToken.None);
var devices = await response.Content.ReadFromJsonAsync<DeviceItemDto[]>();
response.StatusCode.Should().Be(200);
devices.Should().BeEquivalentTo(devicesFromExternalResource);
}
}
Code example
You can customise CustomWebApplicationFactory and FakeHttpMessageHandler according to your test cases, but I hope the idea is clear

Why HttpClient does not hold the base address even when it`s set in Startup

In my .net core web api project I would like to hit an external API so that I get my response as expected.
The way I`m registering and using the HttpClient is as follows. In the startup, I'm adding the following code which is called named typed httpclient way.
services.AddHttpClient<IRecipeService, RecipeService>(c => {
c.BaseAddress = new Uri("https://sooome-api-endpoint.com");
c.DefaultRequestHeaders.Add("x-raay-key", "123567890754545645gggg");
c.DefaultRequestHeaders.Add("x-raay-host", "sooome-api-endpoint.com");
});
In addition to this, I have 1 service in which I inject the HttpClient.
public class RecipeService : IRecipeService
{
private readonly HttpClient _httpClient;
public RecipeService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<Core.Dtos.Recipes>> GetReBasedOnIngAsync(string endpoint)
{
using (var response = await _httpClient.GetAsync(recipeEndpoint))
{
// ...
}
}
}
When the httpClient is created, if I hover over the object itself, the base URI/Headers are missing, and I don't understand why exactly this is happening. I would appreciate if someone could show some light :)
UPDATE 1ST
The service is being used in one of the Controllers shown below. The service is injected by the DI and then the relative path is parsed to the service ( I assumed I already have the base URL stored in the client ) Maybe I`m doing it wrong?
namespace ABC.Controllers
{
[ApiController]
[Route("[controller]")]
public class FridgeIngredientController : ControllerBase
{
private readonly IRecipeService _recipeService;
private readonly IMapper _mapper;
public FridgeIngredientController(IRecipeService recipeService, IMapper mapper)
{
_recipeService = recipeService;
_mapper = mapper;
}
[HttpPost("myingredients")]
public async Task<ActionResult> PostIngredients(IngredientsDto ingredientsDto)
{
var readyUrIngredientStr = string.Join("%2", ingredientsDto.ingredients);
var urlEndpoint = $"recipes/findByIngredients?ingredients={readyUrIngredientStr}";
var recipesResponse = await _recipeService.GetRecipeBasedOnIngredientsAsync(urlEndpoint);
InMyFridgeRecipesDto recipesFoundList = new InMyFridgeRecipesDto
{
FoundRecipes = recipesResponse
};
return Ok(recipesFoundList);
}
}
}
Any suggestions?
A simple, frustrating reason this may happen is due to the order of your service collection statements.
Assigning the dependant service after the HTTPClient will not work, it must come before:
// NOT WORKING - BaseAddress is null
services.AddTransient<Controller1>();
services.AddTransient<Controller2>();
services.AddHttpClient<HttpService>(client =>
{
client.BaseAddress = new Uri(baseAdress);
});
services.AddTransient<HttpService>();
// WORKING - BaseAddress is not null
services.AddTransient<Controller1>();
services.AddTransient<Controller2>();
services.AddTransient<HttpService>();
services.AddHttpClient<HttpService>(client =>
{
client.BaseAddress = new Uri(baseAdress);
});
EDIT
As LIFEfreedom rightfully pointed out in their answer: while the order of the statements has an effect here, it is not the reason for behaviour.
Both of the following statements create a transient service for the HttpService class:
services.AddTransient<HttpService>();
services.AddHttpClient<HttpService>();
However, when adding both of these statements only the latest one will be used, overwriting any statements before it. In my example, I only got the expected result when the AddHttpClient statement with the base address configuration came last.
You configured your client as a typed client and not a named client. No need for the factory.
You should explicitly inject the http client in constructor instead, not the http client factory.
Change your code to this:
private readonly HttpClient _httpClient;
public ReService(HttpClient httpClient;) {
_httpClient = httpClient;
}
public async Task<List<Core.Dtos.Re>> GetReBasedOnIngAsync(string endpoint)
{
///Remove this from your code
var client = _httpClientFactory.CreateClient(); <--- HERE the base URL/Headers are missing
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(endpoint)
};
//////
using (var response = await _httpClient.GetAsync(endpoint))
{
// ...
}
}
And according to last MS documentation only the typed client registration is needed in this case. Fix your startup to this:
// services.AddScoped<IReService, ReService>(); //<-- REMOVE. NOT NEEDED
services.AddHttpClient<IReService, ReService>(c => ...
But you still can try to add you base address, please add trailing slash ( and let us know if it still works):
services.AddHttpClient<IReService, ReService>(c => {
c.BaseAddress = new Uri("https://sooome-api-endpoint.com/");
});
if problem still persists I recommend you to try named http clients.
Okay so I will answer my post because with the suggested TYPED way of doing it was causing problems with the values not being set inside the httpClient, E.G BaseAddress was always null.
In the startup I was trying to go with typed httpclient e.g
services.AddHttpClient<IReService, ReService>(c => ...
But instead of doing that, I choose the to go with the Named client. Which means that in the startup we need to register the httpclient like this
services.AddHttpClient("recipeService", c => {
....
And then in the service itself I used HttpClientFactory like below.
private readonly IHttpClientFactory _httpClientFactory;
public RecipeService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<List<Core.Dtos.Recipes>> GetRecipeBasedOnIngredientsAsync(string recipeEndpoint)
{
var client = _httpClientFactory.CreateClient("recipeService");
using (var response = await client.GetAsync(client.BaseAddress + recipeEndpoint))
{
response.EnsureSuccessStatusCode();
var responseRecipies = await response.Content.ReadAsStringAsync();
var recipeObj = ConvertResponseToObjectList<Core.Dtos.Recipes>(responseRecipies);
return recipeObj ?? null;
}
}
#jack wrote a comment and several guys supported him that this is the right decision, but it is the wrong decision.
AddHttpClient creates a TService service as a Transient service, to which it passes an HttpClient created only for it
Calling first AddTransient, and then AddHttpClient<>, you add 2 implementations of one dependency and only the last added one will be returned
// Create first dependency
services.AddTransient<HttpService>();
// Create second and last dependency
services.AddHttpClient<HttpService>(client =>
{
client.BaseAddress = new Uri(baseAdress);
});

Unit testing a controller action which calls a private method that uses HTTPClient

I am a newbie to C# and TDD. I am developing a product in which I need to write unit tests for some HTTP API calls. Below is how a controller looks like:
public class CommunicationController : ControllerBase
{
private readonly IHttpClientFactory _clientFactory;
private readonly AppSettings _appSettings;
public CommunicationController(IHttpClientFactory clientFactory, IOptions<AppSettings> appSettings)
{
_clientFactory = clientFactory;
_appSettings = appSettings.Value;
}
[HttpPost]
public async Task<IActionResult> PostEntity([FromBody] Entity entity)
{
if (entity.foo == null)
{
NoActionsMessage noActionsMessage = new NoActionsMessage
{
Message = "No actions performed"
};
return Ok(noActionsMessage);
}
var accessTokenDatails = await GetAccessTokenDetailsAsync();
var callUrl = "http://someUrlGoesHere";
var json = JsonConvert.SerializeObject(entity);
var content = new System.Net.Http.StringContent(json, Encoding.UTF8, "application/json");
var request = new HttpRequestMessage(HttpMethod.Put, new Uri(callUrl))
{
Content = content
};
request.Headers.Add("accessToken", accessTokenDatails.AccessToken);
return await InvokeHttpCall(request);
}
private async Task<AccessTokenDetails> GetAccessTokenDetailsAsync()
{
var appId = _appSettings.AppId;
var appSecret = _appSettings.AppSecret;
var refreshToken = _appSettings.RefreshToken;
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("sometokenproviderUrl"));
request.Headers.Add("applicationId", appId);
request.Headers.Add("applicationSecret", appSecret);
request.Headers.Add("refreshToken", refreshToken);
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var responseStream = response.Content.ReadAsStringAsync();
// [ALERT] the failing line in unit test - because responseStream.Result is just a GUID and this the the problem
var result = JsonConvert.DeserializeObject<AccessTokenDetails>(responseStream.Result);
return result;
}
else
{
throw new ArgumentException("Unable to get Access Token");
}
}
}
This POST method which is calling a private method. By calling this post method with appropriate entity given:
1. Should make a call to the token provider service and get the token
2. Using the token, authenticate the service to add the entity
AccessTokenDetails class looks is below:
public sealed class AccessTokenDetails
{
[JsonProperty("accessToken")]
public string AccessToken { get; set; }
[JsonProperty("endpointUrl")]
public Uri EndpointUrl { get; set; }
[JsonProperty("accessTokenExpiry")]
public long AccessTokenExpiry { get; set; }
[JsonProperty("scope")]
public string Scope { get; set; }
}
Now when it comes to unit testing (I am using XUnit) I have a test method like below:
public async Task Entity_Post_Should_Return_OK()
{
/ Arrange - IHttpClientFactoryHttpClientFactory
var httpClientFactory = new Mock<IHttpClientFactory>();
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
var fixture = new Fixture();
mockHttpMessageHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(fixture.Create<string>),
});
var client = new HttpClient(mockHttpMessageHandler.Object);
client.BaseAddress = fixture.Create<Uri>();
httpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);
// Arrange - IOptions
var optionsMock = new Mock<IOptions<AppSettings>>();
optionsMock.SetupGet(o => o.Value).Returns(new AppSettings
{
AppId = "mockappid",
AppSecret = "mockappsecret",
RefreshToken = "mockrefreshtoken"
});
// Arrange - Entity
AddActionEntity entity = new Entity();
entity.foo = "justfoo";
// Act
var controller = new CommunicationController(httpClientFactory.Object, optionsMock.Object);
var result = await controller.PostEntity(entity);
// Assert
Assert.NotNull(result);
Assert.IsAssignableFrom<OkObjectResult>(result);
}
This particular test case is failing in the when calling the PostEntity method as it failed to deserialize the responseStream.Result in the GetAccessTokenDetailsAsync() private method, to AccessTokenDetails in this unit test. The deserialization failed as the value of responseStream.Result is just a GUID string.
Can anyone please tell me that I am getting into a "dependency inversion" problem and tell me a way to overcome this?
I am thinking of separating the GetAccessTokenDetailsAsync to a different class, something like AccessTokenProvider and mock it to over come it - will it be a good approach? what could be a best approach to solve this problem.
ok,let's get a few things straight.
not everything should be unit tested. You have an API and you have a dependency on a token service. Those 2 things need to be integration tested. Mocking and calling API methods won't give you any value.
Unit test business functionality. The moment you start talking about mocking controllers you're going down on a path that serves no real purpose. You need to decouple your business functionality from your controllers
You're not doing TDD. TDD means you're starting with failing tests, the first thing you do is write tests, then start to write code to satisfy those tests. If you had done that from beginning all these issues you uncover now would have been solved already.
Learn how to properly call an API. You mention using responseStream.Result . That's the sign of someone who doesn't know how to use async properly. You need to await your calls properly.
Here's an example based on a quick search : How do I correctly use HttpClient with async/await?
NB. Http client is not supposed to be used inside a using block, that's actually counter productive. Go over this, for example: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
if you want to do proper unit testing, then stop thinking in terms of controllers and start thinking in terms of functionality. You do not need to mock a controller if your code is properly separated. You can simply unit tests those separate classes / libraries outside of your API.
if you want the certainty that your API actually works, stop mocking calls. Make real calls to it, plan your inputs and check the outputs. That's why I said that you integration test endpoints.
Same applies to the token endpoints. Use real calls, get real tokens and see what happens when things go wrong.

Test Polly retry polly configured via Startup.ConfigureServices() with ASP.NET Core API

I want to find out how Polly retry polly configured via Startup.ConfigureServices() can be tested.
ConfigureServices
Polly policy is configured within it
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<IHttpClientService, HttpClientService>()
.SetWaitAndRetryPolicy1();
}
}
Below is the Polly policy:
public static class IServiceCollectionExtension
{
public static void SetWaitAndRetryPolicy1(this IHttpClientBuilder clientBuilder)
{
clientBuilder.AddPolicyHandler((service, request) =>
HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3,
retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount)),
onRetry: (outcome, timespan, retryCount, context) =>
{
service.GetService<ILog>().Error("Delaying for {delay}ms, then making retry {retry}.",
timespan.TotalMilliseconds, retryCount);
}
)
);
}
}
Below is what I tried:
Integration test
The Polly policy is configured within the test.
public class RetryPolicyTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public RetryPolicyTests(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("http://localhost:1234/api/v1/car/")]
public async Task Test3(string url)
{
// Arrange
var client = _factory.WithWebHostBuilder(whb =>
{
whb.ConfigureServices((bc, sc) =>
{
sc.AddOptions();
sc.AddHttpClient("test")
.SetWaitAndRetryPolicy1(); //Test the Polly policy
sc.BuildServiceProvider();
});
})
.CreateClient(); //cannot get a named or typed HttpClient
// Act
var body = "{}";
using (var content = new StringContent(body, Encoding.UTF8, "application/json"))
{
var response = await client.PostAsync(url, content);
}
//Assert: somewhy assert it
}
}
}
The problem is that
I cannot retrieve the HttpClient that has been configured with the Polly polly. Because WebApplicationFactory.CreateClient() has no overloads that returns a named or typed HttpClient:
Any idea?
Is there a better way to testing it?
ASPS.NET Core API 2.2
To modify your posted code minimally to obtain the named or typed HttpClient configured on HttpClientFactory, build the IServiceProvider, obtain the IHttpClientFactory and then obtain the configured client from IHttpClientFactory.
var configuredClient = sc.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient("test");
Many people consider the use of IServiceProvider like this to be a service-locator anti-pattern in production code; perhaps it is ok here in a test, to pull the specific item you want to unit-test out of the default app configuration. However, there are also shorter ways for a test to get a sample HttpClient configured on HttpClientFactory, without using a full WebApplicationFactory (see last part of answer).
For a full end-to-end integration test, testing how your app uses the configured policy, using WebApplicationFactory to exercise some endpoint of your app like http://localhost:1234/api/v1/car/:
You could - within the integration test - use a tool like Mountebank for .NET or HttpClientInterception to stub out the calls that the configured HttpClient makes, so that those calls return errors which you expect the policy to handle.
You could use the ability of WebHostBuilder.ConfigureServices(...) to modify the normal startup of your app, to make it easy to assert something to prove the policy was called. For example, you could configure a mock/fake ILog implementation, and assert that the ILog.Error(...) call in your onRetry delegate takes place.
For the shortest-possible, self-contained unit test to test a Polly policy configured on a given HttpClient configuration on HttpClientFactory, you could use a code pattern like below. This only uses IHttpClientFactory and the standard Microsoft DI infrastructure; no web host from ASP.NET.
public class HttpClientFactory_Polly_Policy_Test
{
[Fact]
public async Task Given_a_retry_policy_configured_on_a_named_client_When_call_via_the_named_client_Then_the_policy_is_used()
{
// Given / Arrange
IServiceCollection services = new ServiceCollection();
bool retryCalled = false;
HttpStatusCode codeHandledByPolicy = HttpStatusCode.InternalServerError;
const string TestClient = "TestClient";
services.AddHttpClient(TestClient)
.AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError()
.RetryAsync(3, onRetry: (_, __) => retryCalled = true))
.AddHttpMessageHandler(() => new StubDelegatingHandler(codeHandledByPolicy));
HttpClient configuredClient =
services
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient(TestClient);
// When / Act
var result = await configuredClient.GetAsync("https://www.doesnotmatterwhatthisis.com/");
// Then / Assert
Assert.Equal(codeHandledByPolicy, result.StatusCode);
Assert.True(retryCalled);
}
}
public class StubDelegatingHandler : DelegatingHandler
{
private readonly HttpStatusCode stubHttpStatusCode;
public StubDelegatingHandler(HttpStatusCode stubHttpStatusCode) => this.stubHttpStatusCode = stubHttpStatusCode;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage(stubHttpStatusCode));
}
If the declarations of policies are pulled out to methods (like SetWaitAndRetryPolicy1() in your posted code), an approach like above provides a more unit-test-focused way to test them.

Moq: How to test a class using Nunit with an internal HttpClient?

I run my tests inside nUnit and normally I can mock out dependencies and have then Return certain values or throw errors.
I have a class that as an internal HttpClient and I would like to test the class, what are my options.
here is my code, its not complete so as not to flood the message. As you can see I am using the HttpClient internally and not injected as a dependency. The class throws a number of custom exceptions, I would like to Moq these otherwise I need to pass REAL username and passwords that would give me the status codes i required to throw the exceptions.
Anyone have an ideas? If I can't mock the httpclient then i can never test my class that it raises exceptions.
Do I really have to change HttpClient to a dependency on the constructor ?
public bool ItemsExist(string itemValue)
{
var relativeUri = string.Format(UrlFormatString, itemValue.ToUpper());
var uri = new Uri(new Uri(this.baseUrl), relativeUri);
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", this.encodedCredentials);
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
var response = client.GetAsync(uri).Result;
switch (response.StatusCode)
{
case HttpStatusCode.Unauthorized:
// DO something here
throw new CustomAuthorizationException();
case HttpStatusCode.Forbidden:
throw new CustomAuthenticationException();
}
return true;
Let me suggest a bit easier solution, without a need to abstract/wrap httpclient, that i believe works perfectly with mocking frameworks.
You need to create a class for fake HttpMessageHandler, like here:
public class FakeHttpMessageHandler : HttpMessageHandler
{
public virtual HttpResponseMessage Send(HttpRequestMessage request)
{
throw new NotImplementedException("Rember to setup this method with your mocking framework");
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
return Task.FromResult(Send(request));
}
}
Such created HttpMessageHandler can be used when instantiating HttpClient:
var msgHandler = new Mock<FakeHttpMessageHandler>() { CallBase = true };
var fakeclient = new HttpClient(msgHandler.Object);
And you can setup methods (here using Moq):
msgHandler.Setup(t => t.Send(It.Is<HttpRequestMessage>(
msg =>
msg.Method == HttpMethod.Post &&
msg.RequestUri.ToString() == "http://test.te/item/123")))
.Returns(new HttpResponseMessage(System.Net.HttpStatusCode.NotFound));
You can now user fakeclient instead when necessary.
You can't unit test it like that. It's like you mentioned: HttpClient is a dependency, and as such, it should be injected.
Personally, I would create my own IHttpClient interface, implemented by HttpClientWrapper, which wraps around the System.Net.HttpClient. IHttpClient would then be passed as a dependency to your object's contructor.
As follows, HttpClientWrapper can't be unit tested. I would, however, write a couple of integration tests to make sure the wrapper is well written.
Edit:
IHttpClient doesn't have to be a "valid" interface for HttpClient. It only has to be an interface that suits your needs. It can have as many or as few methods as you want.
Picture this: HttpClient allows you to do many things. But in your project, you're only calling the GetAsync(uri).Result method, nothing else.
Given this scenario, you would write the following interface and implementation:
interface IHttpClient
{
HttpResponseMessage Get(string uri);
}
class HttpClientWrapper : IHttpClient
{
private readonly HttpClient _client;
public HttpClientWrapper(HttpClient client)
{
_client = client;
}
public HttpResponseMessage Get(string uri)
{
return _client.GetAsync(new Uri(uri)).Result;
}
}
So, as I stated previously, the interface only has to suit your needs. You don't have to wrap around the WHOLE HttpClient class.
Obviously, you would then moq your object like this:
var clientMock = new Mock<IHttpClient>();
//setup mock
var myobj = new MyClass(clientMock.object);
And to create an actual object:
var client = new HttpClientWrapper(new HttpClient());
var myobj = new MyClass(client );
Edit2
OH! And don't forget that IHttpClient should also extend the IDisposable interface, very important!
Another option is to use Flurl [disclosure: I'm the author], a library for building and calling URLs. It includes testing helpers that make faking all HTTP incredibly easy. No need for wrapper interfaces.
For starters, your HTTP code itself would look something like this:
using Flurl;
using Flurl.Http;
...
try {
var response = this.baseUrl
.AppendPathSegment(relativeUri)
.WithBasicAuth(username, password)
.WithHeader("Accept", "application/json")
.GetAsync().Result;
return true;
}
catch (FlurlHttpException ex) {
// Flurl throws on unsuccessful responses. Null-check ex.Response,
// then do your switch on ex.Response.StatusCode.
}
Now for the testing fun:
using Flurl.Http.Testing;
...
[Test]
public void ItemsExists_SuccessResponse() {
// kick Flurl into test mode - all HTTP calls will be faked and recorded
using (var httpTest = new HttpTest()) {
// arrange
test.RespondWith(200, "{status:'ok'}");
// act
sut.ItemExists("blah");
// assert
test.ShouldHaveCalled("http://your-url/*");
}
}
Get it on NuGet:
PM> Install-Package Flurl.Http

Categories

Resources