feat: Make DownstreamApi public to allow easy mocking in tests
closes #3199
@Meir017 : would you like to provide a test, showing how you would use this?
@Meir017 : would you like to provide a test, showing how you would use this?
@jmprieur sure - consider a sample service
public class MyService
{
private readonly IDownstreamApi _downstreamApi;
private readonly ILogger<MyService> _logger;
private const string SampleApiName = "MySampleApi"; // Sample API name
public MyService(IDownstreamApi downstreamApi, ILogger<MyService> logger)
{
_downstreamApi = downstreamApi ?? throw new ArgumentNullException(nameof(downstreamApi));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<string> GetDataFromDownstreamApi(object payload = null)
{
try
{
var result = await _downstreamApi.CallApiForUserAsync(SampleApiName, options =>
{
if (payload != null)
{
options.HttpMethod = HttpMethod.Post;
options.Content = JsonContent.Create(payload);
}
});
if (result.StatusCode == HttpStatusCode.OK)
{
return await result.Content.ReadAsStringAsync();
}
else
{
_logger.LogError($"Downstream API call failed with status code: {result.StatusCode}");
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calling downstream API.");
return null;
}
}
}
and example unit-tests
public class MyServiceTests
{
[Fact]
public async Task GetDataFromDownstreamApi_Success_ReturnsData()
{
// Arrange
var mockDownstreamApi = new Mock<IDownstreamApi>();
var mockLogger = new Mock<ILogger<MyService>>();
var expectedData = "{\"key\": \"value\"}";
var mockHttpResponse = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(expectedData)
};
// a bit confusing finding the example method to mock
mockDownstreamApi.Setup(api => api.CallApiForUserAsync(
"MySampleApi", // Using the sample API name
It.IsAny<Action<DownstreamApiOptions>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockHttpResponse);
var service = new MyService(mockDownstreamApi.Object, mockLogger.Object);
// Act
var result = await service.GetDataFromDownstreamApi();
// Assert
Assert.Equal(expectedData, result);
// a bit confusing finding the example method to verify
mockDownstreamApi.Verify(api => api.CallApiForUserAsync(
"MySampleApi",
It.IsAny<Action<DownstreamApiOptions>>(),
It.IsAny<CancellationToken>()), Times.Once);
}
// NEW WAY
[Fact]
public async Task GetDataFromDownstreamApi_Success_ReturnsData_UsingCallApiInternalAsync()
{
// Arrange
var mockDownstreamApi = new Mock<IDownstreamApi>();
var mockLogger = new Mock<ILogger<MyService>>();
var expectedData = "{\"key\": \"value\"}";
var mockHttpResponse = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(expectedData)
};
// a bit confusing finding the example method to mock
mockDownstreamApi.Setup(api => api.CallApiInternalAsync(
"MySampleApi",
It.IsAny<DownstreamApiOptions>(),
It.IsAny<bool>()),
It.IsAny<JsonContent>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>())
.ReturnsAsync(mockHttpResponse);
var service = new MyService(mockDownstreamApi.Object, mockLogger.Object);
// Act
var result = await service.GetDataFromDownstreamApi();
// Assert
Assert.Equal(expectedData, result);
mockDownstreamApi.Verify(api => api.CallApiInternalAsync(
"MySampleApi",
It.IsAny<DownstreamApiOptions>(),
It.IsAny<bool>()),
It.IsAny<JsonContent>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>(), Times.Once);
}
}
having a single method that is well defined that will be used for mocking will minimize the mocking to a minimum (still running the MergeOptions method and the setup)
in your sample you use a Mock? not the new virtual?
Oh, my bad.
The idea would be to mock the virtual method instead. Making the method virtual allows mocking it
**modified the code sample to highlight the new approach