簡體   English   中英

如何模擬 Microsoft Graph API SDK 客戶端?

[英]How to mock Microsoft Graph API SDK Client?

我在我的項目中使用了 Microsoft Graph SDK 來調用圖形 API,為此我需要使用 GraphServiceClient。 要使用 GraphServiceClient,我必須添加一些輔助類,其中 SDKHelper 是具有 GetAuthenticatedClient() 方法的靜態類。 由於被測方法與靜態的 SDKHelper 緊密耦合,因此我創建了一個服務類並注入了依賴項。

下面是控制器和方法,

public class MyController
{
    private IMyServices _iMyServices { get; set; }

    public UserController(IMyServices iMyServices)
    {
        _iMyServices = iMyServices;
    }
    public async Task<HttpResponseMessage> GetGroupMembers([FromUri]string groupID)
    {
        GraphServiceClient graphClient = _iMyServices.GetAuthenticatedClient();
        IGroupMembersCollectionWithReferencesPage groupMembers = await _iMyServices.GetGroupMembersCollectionWithReferencePage(graphClient, groupID);
        return this.Request.CreateResponse(HttpStatusCode.OK, groupMembers, "application/json");
    }
}

服務類,

public class MyServices : IMyServices
{
    public GraphServiceClient GetAuthenticatedClient()
    {
        GraphServiceClient graphClient = new GraphServiceClient(
            new DelegateAuthenticationProvider(
                async (requestMessage) =>
                {
                    string accessToken = await SampleAuthProvider.Instance.GetAccessTokenAsync();
                    requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
                    requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
                }));
        return graphClient;
    }

    public async Task<IGraphServiceGroupsCollectionPage> GetGraphServiceGroupCollectionPage(GraphServiceClient graphClient)
    {
        return await graphClient.Groups.Request().GetAsync();
    }
}

我在為上述服務類方法編寫單元測試用例時遇到了挑戰,以下是我的單元測試代碼:

public async Task GetGroupMembersCollectionWithReferencePage_Success()
{
    GraphServiceClient graphClient = GetAuthenticatedClient();
    IGraphServiceGroupsCollectionPage groupMembers = await graphClient.Groups.Request().GetAsync();

    Mock<IUserServices> mockIUserService = new Mock<IUserServices>();
    IGraphServiceGroupsCollectionPage expectedResult = await mockIUserService.Object.GetGraphServiceGroupCollectionPage(graphClient);
    Assert.AreEqual(expectedResult, groupMembers);
}

在上面的測試用例中,第 4 行引發異常 - 消息:“Connect3W.UserMgt.Api.Helpers.SampleAuthProvider”的類型初始值設定項引發異常。 內部異常消息:值不能為空。 參數名稱:格式

誰能建議我如何使用 MOQ 來模擬上面的代碼或任何其他方法來完成測試用例?

不要嘲笑你不擁有的東西。 GraphServiceClient應被視為第 3 方依賴項,並應封裝在您控制的抽象之后

您嘗試這樣做,但仍在泄漏實現問題。

該服務可以簡化為

public interface IUserServices {

    Task<IGroupMembersCollectionWithReferencesPage> GetGroupMembers(string groupID);

}

和實施

public class UserServices : IUserServices {
    GraphServiceClient GetAuthenticatedClient() {
        var graphClient = new GraphServiceClient(
            new DelegateAuthenticationProvider(
                async (requestMessage) =>
                {
                    string accessToken = await SampleAuthProvider.Instance.GetAccessTokenAsync();
                    requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
                    requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
                }));
        return graphClient;
    }

    public Task<IGroupMembersCollectionWithReferencesPage> GetGroupMembers(string groupID) {
        var graphClient = GetAuthenticatedClient();
        return graphClient.Groups[groupID].Members.Request().GetAsync();
    }
}

這也會導致控制器被簡化

public class UserController : ApiController {
    private readonly IUserServices service;

    public UserController(IUserServices myServices) {
        this.service = myServices;
    }

    public async Task<IHttpActionResult> GetGroupMembers([FromUri]string groupID) {
        IGroupMembersCollectionWithReferencesPage groupMembers = await service.GetGroupMembers(groupID);
        return Ok(groupMembers);
    }
}

現在為了測試控制器,您可以輕松地模擬抽象以按預期運行,以便完成測試,因為控制器與GraphServiceClient 3rd 方依賴項完全分離,並且可以單獨測試控制器。

[TestClass]
public class UserControllerShould {
    [TestMethod]
    public async Task GetGroupMembersCollectionWithReferencePage_Success() {
        //Arrange
        var groupId = "12345";
        var expectedResult = Mock.Of<IGroupMembersCollectionWithReferencesPage>();
        var mockService = new Mock<IUserServices>();
        mockService
            .Setup(_ => _.GetGroupMembers(groupId))
            .ReturnsAsync(expectedResult);

        var controller = new UserController(mockService.Object);

        //Act
        var result = await controller.GetGroupMembers(groupId) as System.Web.Http.Results.OkNegotiatedContentResult<IGroupMembersCollectionWithReferencesPage>;

        //Assert
        Assert.IsNotNull(result);
        var actualResult = result.Content;
        Assert.AreEqual(expectedResult, actualResult);
    }
}

@Nkosi 的替代解決方案。 使用構造函數public GraphServiceClient(IAuthenticationProvider authenticationProvider, IHttpProvider httpProvider = null); 我們可以模擬實際提出的請求。

下面的完整示例。

我們的GraphApiService使用IMemoryCache來緩存來自 ADB2C 的AccessToken和用戶,用於 HTTP 請求的IHttpClientFactory和來自appsettings.json Settings

https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-5.0

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0

    public class GraphApiService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly IMemoryCache _memoryCache;
        private readonly Settings _settings;
        private readonly string _accessToken;

        public GraphApiService(IHttpClientFactory clientFactory, IMemoryCache memoryCache, Settings settings)
        {
            _clientFactory = clientFactory;
            _memoryCache = memoryCache;
            _settings = settings;

            string graphApiAccessTokenCacheEntry;

            // Look for cache key.
            if (!_memoryCache.TryGetValue(CacheKeys.GraphApiAccessToken, out graphApiAccessTokenCacheEntry))
            {
                // Key not in cache, so get data.
                var adb2cTokenResponse = GetAccessTokenAsync().GetAwaiter().GetResult();

                graphApiAccessTokenCacheEntry = adb2cTokenResponse.access_token;

                // Set cache options.
                var cacheEntryOptions = new MemoryCacheEntryOptions()
                    .SetAbsoluteExpiration(TimeSpan.FromSeconds(adb2cTokenResponse.expires_in));

                // Save data in cache.
                _memoryCache.Set(CacheKeys.GraphApiAccessToken, graphApiAccessTokenCacheEntry, cacheEntryOptions);
            }

            _accessToken = graphApiAccessTokenCacheEntry;
        }

        public async Task<List<Adb2cUser>> GetAllUsersAsync(bool refreshCache = false)
        {
            if (refreshCache)
            {
                _memoryCache.Remove(CacheKeys.Adb2cUsers);
            }

            return await _memoryCache.GetOrCreateAsync(CacheKeys.Adb2cUsers, async (entry) =>
            {
                entry.SetAbsoluteExpiration(TimeSpan.FromHours(1));

                var authProvider = new AuthenticationProvider(_accessToken);
                GraphServiceClient graphClient = new GraphServiceClient(authProvider, new HttpClientHttpProvider(_clientFactory.CreateClient()));

                var users = await graphClient.Users
                    .Request()
                    .GetAsync();

                return users.Select(user => new Adb2cUser()
                {
                    Id = Guid.Parse(user.Id),
                    GivenName = user.GivenName,
                    FamilyName = user.Surname,
                }).ToList();
            });
        }

        private async Task<Adb2cTokenResponse> GetAccessTokenAsync()
        {
            var client = _clientFactory.CreateClient();

            var kvpList = new List<KeyValuePair<string, string>>();
            kvpList.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
            kvpList.Add(new KeyValuePair<string, string>("client_id", _settings.AzureAdB2C.ClientId));
            kvpList.Add(new KeyValuePair<string, string>("scope", "https://graph.microsoft.com/.default"));
            kvpList.Add(new KeyValuePair<string, string>("client_secret", _settings.AzureAdB2C.ClientSecret));

#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
            var req = new HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{_settings.AzureAdB2C.Domain}/oauth2/v2.0/token")
            { Content = new FormUrlEncodedContent(kvpList) };
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation

            using var httpResponse = await client.SendAsync(req);

            var response = await httpResponse.Content.ReadAsStringAsync();

            httpResponse.EnsureSuccessStatusCode();

            var adb2cTokenResponse = JsonSerializer.Deserialize<Adb2cTokenResponse>(response);

            return adb2cTokenResponse;
        }
    }

    public class AuthenticationProvider : IAuthenticationProvider
    {
        private readonly string _accessToken;

        public AuthenticationProvider(string accessToken)
        {
            _accessToken = accessToken;
        }

        public Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            request.Headers.Add("Authorization", $"Bearer {_accessToken}");

            return Task.CompletedTask;
        }
    }

    public class HttpClientHttpProvider : IHttpProvider
    {
        private readonly HttpClient http;

        public HttpClientHttpProvider(HttpClient http)
        {
            this.http = http;
        }

        public ISerializer Serializer { get; } = new Serializer();

        public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);

        public void Dispose()
        {
        }

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
        {
            return http.SendAsync(request);
        }

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            HttpCompletionOption completionOption,
            CancellationToken cancellationToken)
        {
            return http.SendAsync(request, completionOption, cancellationToken);
        }
    }

然后我們在各種Controllers使用GraphApiService 下面是一個簡單的CommentController示例。 不包括CommentService但無論如何示例都不需要它。

[Route("api/[controller]")]
[ApiController]
[Authorize]
public class CommentController : ControllerBase
{
    private readonly CommentService _commentService;
    private readonly GraphApiService _graphApiService;

    public CommentController(CommentService commentService, GraphApiService graphApiService)
    {
        _commentService = commentService;
        _graphApiService = graphApiService;
    }

    [HttpGet("{rootEntity}/{id}")]
    public ActionResult<IEnumerable<CommentDto>> Get(RootEntity rootEntity, int id)
    {
        var comments = _commentService.Get(rootEntity, id);

        var users = _graphApiService.GetAllUsersAsync().GetAwaiter().GetResult();

        var commentDtos = new List<CommentDto>();

        foreach (var comment in comments)
        {
            commentDtos.Add(CommonToDtoMapper.MapCommentToCommentDto(comment, users));
        }

        return Ok(commentDtos);
    }

    [HttpPost("{rootEntity}/{id}")]
    public ActionResult Post(RootEntity rootEntity, int id, [FromBody] string message)
    {
        _commentService.Add(rootEntity, id, message);
        _commentService.SaveChanges();

        return Ok();
    }
}

由於我們使用自己的IAuthenticationProviderIHttpProvider我們可以根據調用的 URI 來模擬IHttpClientFactory 下面完整的測試示例,檢查mockMessageHandler.Protected()以查看請求是如何mockMessageHandler.Protected() 為了找到確切的請求,我們查看了文檔。 例如var users = await graphClient.Users.Request().GetAsync(); 相當於GET https://graph.microsoft.com/v1.0/users

https://docs.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0&tabs=http#request

    public class CommentControllerTest : SeededDatabase
    {
        [Fact]
        public void Get()
        {
            using (var context = new ApplicationDbContext(_dbContextOptions))
            {
                var controller = GeCommentController(context);
                var result = controller.Get(RootEntity.Question, 1).Result;

                var okResult = Assert.IsType<OkObjectResult>(result);
                var returnValue = Assert.IsType<List<CommentDto>>(okResult.Value);

                Assert.Equal(2, returnValue.Count());
            }
        }

        [Theory]
        [MemberData(nameof(PostData))]
        public void Post(RootEntity rootEntity, int id, string message)
        {
            using (var context = new ApplicationDbContext(_dbContextOptions))
            {
                var controller = GeCommentController(context);

                var result = controller.Post(rootEntity, id, message);

                var okResult = Assert.IsType<OkResult>(result);

                var comment = context.Comments.First(x => x.Text == message);

                if(rootEntity == RootEntity.Question)
                {
                    Assert.Equal(comment.QuestionComments.First().QuestionId, id);
                }
            }
        }

        public static IEnumerable<object[]> PostData()
        {
            return new List<object[]>
                {
                    new object[]
                    { RootEntity.Question, 1, "Test comment from PostData" }
                };
        }

        private CommentController GeCommentController(ApplicationDbContext dbContext)
        {
            var userService = new Mock<IUserResolverService>();
            userService.Setup(x => x.GetNameIdentifier()).Returns(DbContextSeed.CurrentUser);

            var settings = new Settings();

            var commentService = new CommentService(new ExtendedApplicationDbContext(dbContext, userService.Object));

            var expectedContentGetAccessTokenAsync = @"{
    ""token_type"": ""Bearer"",
    ""expires_in"": 3599,
    ""ext_expires_in"": 3599,
    ""access_token"": ""123""
}";

            var expectedContentGetAllUsersAsync = @"{
    ""@odata.context"": ""https://graph.microsoft.com/v1.0/$metadata#users"",
    ""value"": [
        {
            ""businessPhones"": [],
            ""displayName"": ""Oscar"",
            ""givenName"": ""Oscar"",
            ""jobTitle"": null,
            ""mail"": null,
            ""mobilePhone"": null,
            ""officeLocation"": null,
            ""preferredLanguage"": null,
            ""surname"": ""Andersson"",
            ""userPrincipalName"": """ + DbContextSeed.DummyUserExternalId + @"@contoso.onmicrosoft.com"",
            ""id"":""" + DbContextSeed.DummyUserExternalId + @"""
        }
    ]
}";

            var mockFactory = new Mock<IHttpClientFactory>();

            var mockMessageHandler = new Mock<HttpMessageHandler>();
            mockMessageHandler.Protected()
#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.Contains("https://login.microsoftonline.com/")), ItExpr.IsAny<CancellationToken>())
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(expectedContentGetAccessTokenAsync)
                });

            mockMessageHandler.Protected()
#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.Contains("https://graph.microsoft.com/")), ItExpr.IsAny<CancellationToken>())
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(expectedContentGetAllUsersAsync)
                });

            var httpClient = new HttpClient(mockMessageHandler.Object);

            mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);

            var services = new ServiceCollection();
            services.AddMemoryCache();
            var serviceProvider = services.BuildServiceProvider();

            var memoryCache = serviceProvider.GetService<IMemoryCache>();

            var graphService = new GraphApiService(mockFactory.Object, memoryCache, settings);

            var controller = new CommentController(commentService, graphService);

            return controller;
        }
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM