如何模拟(最小起订量)IConfidentialClientApplication 已密封设置 AbstractAcquireTokenParameterBuilder?

How to mock (MOQ) IConfidentialClientApplication which has sealed setup AbstractAcquireTokenParameterBuilder?

我在尝试为 IConfidentialClientApplication 设置 moq 时遇到以下异常:

System.NotSupportedException : Unsupported expression: ... => ....ExecuteAsync() Non-overridable members (here: AbstractAcquireTokenParameterBuilder.ExecuteAsync) may not be used in setup / verification expressions.

private Mock<IConfidentialClientApplication> _appMock = new Mock<IConfidentialClientApplication>();

[Fact]
public async Task GetAccessTokenResultAsync_WithGoodSetup_ReturnsToken()
{
    // Leverages MSAL AuthenticationResult constructor meant for mocks in test
    var authentication = CreateAuthenticationResult();

    // EXCEPTION THROWN HERE
    _appMock.Setup(_ => _.AcquireTokenForClient(It.IsAny<string[]>()).ExecuteAsync())
        .ReturnsAsync(authentication);

    ... rest of test ...
}

_.AcquireTokenForClient返回一个AcquireTokenForClientParameterBuilder; “使您能够在执行令牌请求之前添加可选参数的构建器”。 This is a sealed class,所以我不能轻易地模拟这个棘手的对象。


对于那些好奇的人来说,CreateAuthenticationResult() 是一种从 Microsoft.Identity.Client.AuthenticationResult 调用签名的方法,微软专门添加了该签名用于存根 AuthenticationResult,因为它不能被模拟,因为它也是密封的class.

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/682

看到 AcquireTokenForClientParameterBuilder 是通过外部库提供的,您显然无法修改它以使其更易于测试。鉴于此,我建议在您自己的界面后面抽象该代码(出于测试目的应用适配器模式)。

以下面的 service/test 为例,说明您当前如何使用 IConfidentialClientApplication 并尝试模拟它(这会导致您看到的相同错误):

public class MyService
{
    private readonly IConfidentialClientApplication _confidentialClientApplication;

    public MyService(IConfidentialClientApplication confidentialClientApplication)
    {
        _confidentialClientApplication = confidentialClientApplication;
    }

    public async Task<string> GetAccessToken(IEnumerable<string> scopes)
    {
        AcquireTokenForClientParameterBuilder tokenBuilder = _confidentialClientApplication.AcquireTokenForClient(scopes);
        AuthenticationResult token = await tokenBuilder.ExecuteAsync();
        return token.AccessToken;
    }
}

public class UnitTest1
{
    [Fact]
    public async Task Test1()
    {
        Mock<IConfidentialClientApplication> _appMock = new Mock<IConfidentialClientApplication>();
        AuthenticationResult authentication = CreateAuthenticationResult("myToken");
        _appMock
            .Setup(_ => _.AcquireTokenForClient(It.IsAny<string[]>()).ExecuteAsync())
            .ReturnsAsync(authentication);

        var myService = new MyService(_appMock.Object);
        string accessToken = await myService.GetAccessToken(new string[] { });

        Assert.Equal("myToken", accessToken);
    }

    private AuthenticationResult CreateAuthenticationResult(string accessToken) => 
        new AuthenticationResult(accessToken, true, null, DateTimeOffset.Now, DateTimeOffset.Now, string.Empty, null, null, null, Guid.Empty);
}

通过引入一个单独的接口,您的代码可以简单地依赖于它,让您可以控制它的方式 used/tested:

public interface IIdentityClientAdapter
{
    Task<string> GetAccessToken(IEnumerable<string> scopes);
}

public class IdentityClientAdapter : IIdentityClientAdapter
{
    private readonly IConfidentialClientApplication _confidentialClientApplication;

    public IdentityClientAdapter(IConfidentialClientApplication confidentialClientApplication)
    {
        _confidentialClientApplication = confidentialClientApplication;
    }

    public async Task<string> GetAccessToken(IEnumerable<string> scopes)
    {
        AcquireTokenForClientParameterBuilder tokenBuilder = _confidentialClientApplication.AcquireTokenForClient(scopes);
        AuthenticationResult token = await tokenBuilder.ExecuteAsync();
        return token.AccessToken;
    }
}

public class MyService
{
    private readonly IIdentityClientAdapter _identityClientAdapter;

    public MyService(IIdentityClientAdapter identityClientAdapter)
    {
        _identityClientAdapter = identityClientAdapter;
    }

    public async Task<string> GetAccessToken(IEnumerable<string> scopes)
    {
        return await _identityClientAdapter.GetAccessToken(scopes);
    }
}

public class UnitTest1
{
    [Fact]
    public async Task Test1()
    {
        Mock<IIdentityClientAdapter> _appMock = new Mock<IIdentityClientAdapter>();
        _appMock
            .Setup(_ => _.GetAccessToken(It.IsAny<string[]>()))
            .ReturnsAsync("myToken");

        var myService = new MyService(_appMock.Object);
        string accessToken = await myService.GetAccessToken(new string[] { });

        Assert.Equal("myToken", accessToken);
    }
}

这个例子显然是微不足道的,但应该仍然适用。该界面只需要适合您的需求。