如何模拟Microsoft Graph API SDK客户端?

时间:2018-01-11 13:16:51

标签: c# asp.net-web-api moq microsoft-graph

我在我的项目中使用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行引发异常 - 消息:&#39; Connect3M.UserMgt.Api.Helpers.SampleAuthProvider&#39;的类型初始值设定项抛出一个例外。 内部异常消息:值不能为空。参数名称:格式

有人可以建议我如何使用MOQ来模拟上面的代码或任何其他方法来完成测试用例吗?

2 个答案:

答案 0 :(得分:1)

不要嘲笑你不拥有的东西。 GraphServiceClient应被视为第三方依赖,应该封装在您控制的抽象之后

您试图这样做,但仍然在泄漏实施问题。

服务可以简化为

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第三方依赖关系完全分离,控制器可以在隔离。

[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);
    }
}

答案 1 :(得分:0)

@Nkosi 的替代解决方案。使用构造函数 public GraphServiceClient(IAuthenticationProvider authenticationProvider, IHttpProvider httpProvider = null); 我们可以模拟实际发出的请求。

完整示例如下。

我们的 GraphApiService 使用 IMemoryCache,缓存 AccessToken 和来自 ADB2C 的用户,IHttpClientFactory 用于 HTTP 请求,Settings 来自 appsettings.json .

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);
        }
    }

然后我们在各种 GraphApiService 中使用 Controllers。下面是一个简单的 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() 以查看请求是如何模拟的。为了找到确切的请求,我们查看了文档。例如 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;
        }
    }