microsoft-identity-web icon indicating copy to clipboard operation
microsoft-identity-web copied to clipboard

feat: Make DownstreamApi public to allow easy mocking in tests

Open Meir017 opened this issue 9 months ago • 4 comments

closes #3199

Meir017 avatar Mar 17 '25 18:03 Meir017

@Meir017 : would you like to provide a test, showing how you would use this?

jmprieur avatar Mar 17 '25 19:03 jmprieur

@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)

Meir017 avatar Mar 17 '25 19:03 Meir017

in your sample you use a Mock? not the new virtual?

jmprieur avatar Mar 17 '25 19:03 jmprieur

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

Meir017 avatar Mar 17 '25 19:03 Meir017