How to mock Microsoft Graph API SDK Client?
Asked Answered
I

3

10

I have used Microsoft Graph SDK in my project to call graph API, for this I need to use GraphServiceClient. To use GraphServiceClient, i have to add some helper classes, in which SDKHelper is a static class which has GetAuthenticatedClient() method. Since method under test is tightly coupled to SDKHelper which is static, so I have created a service class and injected the dependency.

Below is the controller and method,

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

Service Class,

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

I am having challenge in writing Unit Test Case for the above service class methods, Below is my Unit Test Code:

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

In Above Test case line number 4 throws an exception - Message: The type initializer for 'Connect3W.UserMgt.Api.Helpers.SampleAuthProvider' threw an exception. Inner Exception Message: Value cannot be null. Parameter name: format

Can anyone suggest me how to use MOQ to mock above code or any other method to complete test case for this ?

Instant answered 11/1, 2018 at 13:16 Comment(3)
Provide a minimal reproducible example that can be used to reproduce the error. You have not provided enough context in the current question.Century
The mehod under test seems tightly coupled to SDKHelper which appears to be static (guessing here since not enough details provided). This would make mocking desired behavior difficult.Century
@Century i have loosely coupled this by adding service class but now i have to write test case for this service, where i am having same challengeInstant
C
18

Do not mock what you do not own. GraphServiceClient should be treated as a 3rd party dependency and should be encapsulated behind abstractions you control

You attempted to do that but are still leaking implementation concerns.

The service can be simplified to

public interface IUserServices {

    Task<IGroupMembersCollectionWithReferencesPage> GetGroupMembers(string groupID);

}

and the implementation

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

Which would result in the controller being simplified as well

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

Now for testing of the controller you can easily mock the abstractions to behave as expected in order to exercise the test to completion because the controller is completely decoupled from the GraphServiceClient 3rd party dependency and the controller can be tested in isolation.

[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);
    }
}
Century answered 11/1, 2018 at 15:6 Comment(3)
we will again then have problem writing test cases for UserServices class, as it has GetAuthenticatedClient method in it. So in this case we need to exclude this method for not having test casesInstant
You do not need to unit test User Service as it is just wrapping the 3rd party dependency. To test it you would need to do integration testing which would require more resources configured to successfully test it.Century
This is a great example of how TDD drives a developer into finding better abstractions imho. If it is not easy to mock, the abstraction is wrong. In this case it would seem natural to break the calling code into its own layer.Erythropoiesis
W
1

An alternative solution to @Nkosi. Using the constructor public GraphServiceClient(IAuthenticationProvider authenticationProvider, IHttpProvider httpProvider = null); we can Mock the requests actually made.

Complete example below.

Our GraphApiService uses IMemoryCache, to cache both AccessToken and Users from ADB2C, IHttpClientFactory for HTTP requests and Settings is from appsettings.json.

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

https://learn.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);
        }
    }

We then use the GraphApiService in various Controllers. Example from a simple CommentController below. CommentService not included but it is not needed for the example anyway.

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

Since we use our own IAuthenticationProvider and IHttpProvider we can mock IHttpClientFactory based on what URI is called. Complete test example below, check mockMessageHandler.Protected() to see how the requests are mocked. To find the exact request made we look at the documentation. For example var users = await graphClient.Users.Request().GetAsync(); is equivalent to GET https://graph.microsoft.com/v1.0/users.

https://learn.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;
        }
    }
Watercress answered 20/4, 2021 at 9:51 Comment(0)
P
0

These answers above are effective, but if you do want to do some specific functionality without the HttpClient Management you can create a wrapper and inject it into your services, mocking it as usual. IE:

//HACK - This essentially operates as a wrapper to allow us to mock the GraphServiceClient using the interface. If you add any requests that are not users you have to add them here. This example just adds the Users functionality
public class GraphServiceClientWrapper : GraphServiceClient, IGraphServiceClient
{
    public GraphServiceClientWrapper(TokenCredential tokenCredential, IEnumerable<string>? scopes = null, string? baseUrl = null) : base(tokenCredential, scopes, baseUrl)
    {
    }
    public new UsersRequestBuilder Users
    {
        get => new UsersRequestBuilder(PathParameters, RequestAdapter);
    }
}

With the interface:

public interface IGraphServiceClient
{
    UsersRequestBuilder Users { get; }

}

With the dependency injection:

 IGraphServiceClient graphServiceClient = new GraphServiceClientWrapper(new ClientSecretCredential(_adOptionsReference.TenantId, _adOptionsReference.ClientId, _adOptionsReference.ClientSecret), ["https://graph.microsoft.com/.default"]);
 serviceCollection.AddSingleton(graphServiceClient);

Usable in unit tests as such:

private readonly Mock<IGraphServiceClient> _graphServiceClient;
private readonly MyServiceUsingGraph _myServiceUsingGraph;
public MyUnitTest()
{
    _graphServiceClient = new Mock<IGraphServiceClient>();

    _myServiceUsingGraph = new MyServiceUsingGraph(_graphServiceClient.Object);
}

With the implementation of the service:

public class MyServiceUsingGraph
{
     private readonly IGraphClientService _graphClientService;
     public MyServiceUsingGraph(IGraphClientService graphClientService)
     {
         _graphClientService = graphClientService;
     }
}
Pelagian answered 9/1 at 18:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.