Reputation: 1
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.
Upvotes: 0
Views: 1256
Reputation: 6335
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.
Upvotes: 1