Problems, solutions and recommendations - NextensArelB/SwaggerGenerationTool GitHub Wiki
On this page we list problems and the possible solutions when creating c# Unittets tests.
[[TOC]]
##Problem when you are creating a Unittest for a method that involkes a API call you might encounter that mocking the HTTPClient is not a easy task for various reasons:
- You need to create the HTTPCLient object using the HTTPClientFactory you might be tempted to do this:
[TestClass]
public class MyApiTestClass
{
private Mock<IHttpClientFactory> _httpClientFactoryMock = null!;
private HttpClient _httpClient = null!;
[TestInitialize]
public void Startup()
{
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
_httpMessageHandler = new Mock<HttpMessageHandler>();
_httpClient = new HttpClient(_httpMessageHandler.Object);
_httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny<string>()))
.Returns(_httpClient).Verifiable();
}
}
- If you perform multiple API calls in the same function you need to set up the HttpMessageHandler to return for each call a specific HttpResponseMessage with a mocked reponse, this clutters your code. You might be tempted to do this:
_httpMessageHandler.Protected()
.SetupSequence<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage() { Content = new StringContent(mockResponseRgsSchemas), StatusCode = HttpStatusCode.OK })
.ReturnsAsync(new HttpResponseMessage() { Content = new StringContent(mockResponseRgsBrugstaat), StatusCode = HttpStatusCode.OK })
.ReturnsAsync(new HttpResponseMessage() { Content = new StringContent(mockDivisionResponse), StatusCode = HttpStatusCode.OK })
.ReturnsAsync(new HttpResponseMessage() { Content = new StringContent(mockDivisionDetailsResponse), StatusCode = HttpStatusCode.OK });
Where the mock reponses are the actual reponse from the API call.
- You need to mock a number of dependent objects
##Analyse Altough the sample above works there is a more elegant and maintainable way of mocking the HttpClient.
##Solution
[TestClass]
public class MyApiTestClass
{
private HttpClient _httpClient;
[TestInitialize]
public async Task Startup()
{
_httpClient = new HttpClient() { BaseAddress = _baseAddress };
_mockHttpFactory = new Mock<IHttpClientFactory>();
_mockHttpFactory.Setup(_ => _.CreateClient(HttpClientNameConstants.SomeHttpClient))
.Returns(_httpClient).Verifiable();
}
[TestMethod]
public async Task GetSomethingFromApi_ReturnsData_ItShouldReturnPopulatedObject()
{
[...]
_httpClient = new HttpClient(new HttpClient_WithResponse_DelegateHandler<string>(HttpStatusCode.OK,
SomeMockResponses.SomeData))
{ BaseAddress = _baseAddress };
_mockHttpFactory.Setup(_ => _.CreateClient(HttpClientNameConstants.VismaHttpClient))
.Returns(_httpClient);
[...]
}
}
This is all possible because we have ceated some Httpclient Delegate classes:
public class HttpClient_WithResponse_DelegateHandler<T> : DelegatingHandler
{
HttpStatusCode _statusCode;
private T _returnObject;
public HttpClient_WithResponse_DelegateHandler(HttpStatusCode statusCode, T returnObject)
{
_statusCode = statusCode;
_returnObject = returnObject;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpContent content;
if (typeof(T) == typeof(String))
{
content = new StringContent(_returnObject as string);
}
else
{
content = JsonContent.Create(_returnObject);
}
HttpResponseMessage response = new()
{
StatusCode = _statusCode,
Content = content
};
return Task.FromResult(response);
}
}
public class HttpClient_WithResponses_DelegateHandler<T> : DelegatingHandler
{
private Dictionary<string, HttpMockResponse<T>> _returnObject;
public HttpClient_WithResponses_DelegateHandler(Dictionary<string, HttpMockResponse<T>> returnObject)
{
_returnObject = returnObject;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpContent content;
HttpStatusCode statusCode;
string key = Uri.UnescapeDataString(request.RequestUri.PathAndQuery);
if (typeof(T) == typeof(String))
{
HttpMockResponse<T> data = _returnObject.Where(x => x.Key == key).Select(x => x.Value).FirstOrDefault();
if (data == null) { throw new ArgumentNullException($"Missing http response for request {key}"); }
content = new StringContent(data.Object as string);
statusCode = data.HttpStatusCode;
}
else
{
HttpMockResponse<T> data = _returnObject.Where(x => x.Key == key).Select(x => x.Value).FirstOrDefault();
if (data == null) { throw new ArgumentNullException($"Missing http response for request {key}"); }
content = JsonContent.Create(data.Object);
statusCode = data.HttpStatusCode;
}
HttpResponseMessage response = new()
{
StatusCode = statusCode,
Content = content
};
return Task.FromResult(response);
}
}
public class HttpMockResponse<ResponseObject>
{
public HttpMockResponse(ResponseObject obj, HttpStatusCode code)
{
Object = obj;
HttpStatusCode = code;
}
public ResponseObject Object { get; private set; }
public HttpStatusCode HttpStatusCode { get; private set; }
}
When your method that you are testing makes multiple API calls to collect data, you can do that in one test function by creating a Dictionary having request Url's and mock reponses and pass that into the HttpClient deligate in one go, for example:
[...]
Dictionary<string, HttpMockResponse<string>> responses = new()
{
{ "/v2/fiscalyears", new HttpMockResponse<string>(VismaMockResponses.Periods, HttpStatusCode.OK) },
{ "/v2/accounts/3d2bac5a-c643-4075-8ae0-943dc1ee65c4?$page=1&$pagesize=1000", new HttpMockResponse<string>(VismaMockResponses.Accounts, HttpStatusCode.OK) },
{ "/v2/accounts/fb581370-5b88-4199-848e-d268e30fd551?$page=1&$pagesize=1000", new HttpMockResponse<string>(VismaMockResponses.Accounts, HttpStatusCode.OK) },
{ "/v2/accountbalances/2018-12-31?$page=1&$pagesize=%message1000", new HttpMockResponse<string>(VismaMockResponses.AccountBalances, HttpStatusCode.OK) },
{ "/v2/accountbalances/2019-12-31?$page=1&$pagesize=%message1000", new HttpMockResponse<string>(VismaMockResponses.AccountBalances, HttpStatusCode.OK) },
{ "/v2/fiscalyears/openingbalances?$page=1&$pagesize=1000", new HttpMockResponse<string>(VismaMockResponses.AccountBalances, HttpStatusCode.OK) },
{ "/v2/companysettings", new HttpMockResponse<string>(VismaMockResponses.CompanySettings, HttpStatusCode.OK) }
};
_httpClient = new HttpClient(new HttpClient_WithResponses_DelegateHandler<string>(responses))
{ BaseAddress = _baseAddress };
_mockHttpFactory.Setup(_ => _.CreateClient(HttpClientNameConstants.VismaHttpClient))
.Returns(_httpClient);
var handler = new VismaFinancial(_loggerMock.Object, _mockHttpFactory.Object, _mockAccessTokenHandler.Object);
// Assert
[...]