Skip to content

feat: Make DownstreamApi public to allow easy mocking in tests #3302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Meir017
Copy link

@Meir017 Meir017 commented Mar 17, 2025

closes #3199

@Meir017 Meir017 requested a review from a team as a code owner March 17, 2025 18:07
@@ -453,7 +453,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
}
}

internal /* for tests */ async Task<HttpResponseMessage> CallApiInternalAsync(
protected virtual /* for tests */ async Task<HttpResponseMessage> CallApiInternalAsync(
Copy link
Author

@Meir017 Meir017 Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmprieur the only required change (the rest are formatting from vscode...)

@jmprieur
Copy link
Collaborator

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

@Meir017
Copy link
Author

Meir017 commented Mar 17, 2025

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

@jmprieur
Copy link
Collaborator

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

@Meir017
Copy link
Author

Meir017 commented Mar 17, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature Request] introduce a base class for IDownstreamApi to simplify mocking in tests
2 participants