Azure / azure-sdk-for-net

This repository is for active development of the Azure SDK for .NET. For consumers of the SDK we recommend visiting our public developer docs at https://learn.microsoft.com/dotnet/azure/ or our versioned developer docs at https://azure.github.io/azure-sdk-for-net.
MIT License
5.23k stars 4.58k forks source link

[QUERY] How to mock ConfigurationClientExtensions.AsPages without access to internal class #44990

Closed amerjusupovic closed 4 weeks ago

amerjusupovic commented 1 month ago

Library name and version

Azure.Data.AppConfiguration 1.4.1

Query/Question

When writing unit tests using Moq there's no way to mock extension methods, and calling this method results in an InvalidOperationException because it tries to cast the pageable object as AsyncConditionalPageable, which is an internal class.

I see this issue is similar, but the tracking issue mentioned in this comment is closed, and it seemed like this might need attention as well.

Environment

.NET SDK: Version: 8.0.300 Commit: 326f6e68b2 Workload version: 8.0.300-manifests.ca8b4b2d MSBuild version: 17.10.4+10fbfbf2e

Runtime Environment: OS Name: Windows OS Version: 10.0.22631 OS Platform: Windows RID: win-x64 Base Path: C:\Program Files\dotnet\sdk\8.0.300\

Visual Studio 2022 - Version 17.10.2

jsquire commented 1 month ago

Hi @amerjusupovic. Can you help me understand the end-to-end scenario of what you're trying to test?

Unless you've got an unusual scenario, you shouldn't need to mock the extensions, but would be mocking the client method to return your own pagable, in a pattern similar to what is demonstrated in [Unit testing and mocking with the Azure SDK for .NET](https://learn.microsoft.com/dotnet/azure/sdk/unit-testing-mocking?tabs=moq#explore-paging:

Page<SecretProperties> page1 = Page<SecretProperties>.FromValues(
    new[]
    {
        new SecretProperties("secret1"),
        new SecretProperties("secret2")
    },
    "continuationToken",
    Mock.Of<Response>());

Page<SecretProperties> page2 = Page<SecretProperties>.FromValues(
    new[]
    {
        new SecretProperties("secret3"),
        new SecretProperties("secret4")
    },
    "continuationToken2",
    Mock.Of<Response>());

Page<SecretProperties> lastPage = Page<SecretProperties>.FromValues(
    new[]
    {
        new SecretProperties("secret5"),
        new SecretProperties("secret6")
    },
    continuationToken: null,
    Mock.Of<Response>());

Pageable<SecretProperties> pageable = Pageable<SecretProperties>
    .FromPages(new[] { page1, page2, lastPage });

AsyncPageable<SecretProperties> asyncPageable = AsyncPageable<SecretProperties>
    .FromPages(new[] { page1, page2, lastPage });
github-actions[bot] commented 1 month ago

Hi @amerjusupovic. Thank you for opening this issue and giving us the opportunity to assist. To help our team better understand your issue and the details of your scenario please provide a response to the question asked above or the information requested above. This will help us more accurately address your issue.

amerjusupovic commented 1 month ago

Thanks, my scenario is that I want to test my code that calls ConfigurationClient.GetConfigurationSettingsAsync.AsPages(IEnumerable<MatchConditions>). Before, I only had calls to AsPages() without any params and that was fine because I could create a mock of AsyncPageable<ConfigurationSetting> and implement a mock AsPages method. However, that doesn't work anymore because I'm not allowed to mock an extension method in Moq and I can't just create a new AsPages on the mock pageable like I have before, since it won't be an extension.

For example, I can't do this:

            var mockClient = new Mock<ConfigurationClient>(MockBehavior.Strict);

            mockClient.Setup(c => c.GetConfigurationSettingsAsync(
                It.IsAny<SettingSelector>(), 
                It.IsAny<CancellationToken>())
            .AsPages(
               It.IsAny<IEnumerable<MatchConditions>>(), 
               It.IsAny<string>(), 
               It.IsAny<int>()))
            // rest of setup

and I can't just mock GetConfigurationSettingsAsync because when it calls the AsPages(IEnumerable<MatchConditions>) extension method, it throws InvalidOperationException.

Let me know if that makes sense or if there's a different way to test the extension.

jsquire commented 1 month ago

@amerjusupovic: In this case, you'd want to combine the approach that I described above along with a custom mock extension method. By defining your extension method in the same namespace as the code calling it and using actual types rather than open generics, it will be the implementation that the compiler prefers.

for example:


Page<ConfigurationSetting> page1 = Page<ConfigurationSetting>.FromValues(
    new[]
    {
        new ConfigurationSetting("secret1", "val1"),
        new ConfigurationSetting("secret2", "val2")
    },
    "continuationToken",
    Mock.Of<Response>());

Page<ConfigurationSetting> page2 = Page<ConfigurationSetting>.FromValues(
    new[]
    {
        new ConfigurationSetting("secret3", "val3"),
        new ConfigurationSetting("secret4", "val4")
    },
    "continuationToken2",
    Mock.Of<Response>());

Page<ConfigurationSetting> lastPage = Page<ConfigurationSetting>.FromValues(
    new[]
    {
        new ConfigurationSetting("secret5", "val5"),
        new ConfigurationSetting("secret6", "val6")
    },
    continuationToken: null,
    Mock.Of<Response>());

AsyncPageable<ConfigurationSetting> asyncPageable = AsyncPageable<ConfigurationSetting>
    .FromPages(new[] { page1, page2, lastPage });

var mockClient = new Mock<ConfigurationClient>();

mockClient
    .Setup(c => c.GetConfigurationSettingsAsync(
        It.IsAny<SettingSelector>(),
        It.IsAny<CancellationToken>()))
    .Returns(asyncPageable);

// Invoke the mocks
var mockPagable = mockClient.Object.GetConfigurationSettingsAsync(new(), default);
var mockPages = mockPagable.AsPages(new[] { new MatchConditions() });

// Mock extensions with more specific typing and namespace locality.
public static class MockExtensions
{
    public static async IAsyncEnumerable<Page<ConfigurationSetting>> AsPages(
        this AsyncPageable<ConfigurationSetting> instance, 
        IEnumerable<MatchConditions> matches, 
        string continuationToken = null, 
        int? pageHint = null)
    {
        await foreach (var page in instance.AsPages())
        {
            yield return page;
        }
    }
}
github-actions[bot] commented 1 month ago

Hi @amerjusupovic. Thank you for opening this issue and giving us the opportunity to assist. We believe that this has been addressed. If you feel that further discussion is needed, please add a comment with the text "/unresolve" to remove the "issue-addressed" label and continue the conversation.

github-actions[bot] commented 4 weeks ago

Hi @amerjusupovic, since you haven’t asked that we /unresolve the issue, we’ll close this out. If you believe further discussion is needed, please add a comment /unresolve to reopen the issue.