C# mock unit test GraphServiceClient
Asked Answered
P

3

8

I have a problem writing my unit test in C# using Moq and xUnit.

In my service I have the following code:

var options = new TokenCredentialOptions
{
    AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};

var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret, options);
var graphClient = new GraphServiceClient(clientSecretCredential);


return (await graphClient.Users.Request().Filter($"displayName eq '{mobilePhone}'").GetAsync()).FirstOrDefault();

But I don't know a method to mock the GraphClient function:

graphClient.Users.Request().Filter($"displayName eq '{mobilePhone}'").GetAsync()).FirstOrDefault();
Polybius answered 21/10, 2021 at 9:46 Comment(3)
Could you elaborate a bit more on what you tried and where you failed? Also, please refer to How to Ask. – Conidiophore
Does this answer your question? How to mock Microsoft Graph API SDK Client? – Conidiophore
@Conidiophore the problem in my case is that i don't know how mock GraphServiceClient in my unit test? i have to wrote a unit test for my method "foo()" inside it, use GraphServiceClient and i don't know how mock the response. – Polybius
F
17

Graph v5 SDK

After upgrading the Graph SDK to v5 the way on how to mock the service client has been changed and it doesn't get easier. There are different approaches available and a few of them are listed in this issue. My current approach of mocking is this here:

public static class RequestAdapterMockFactory
{
    public static Mock<IRequestAdapter> Create(MockBehavior mockBehavior = MockBehavior.Strict)
    {
        var mockSerializationWriterFactory = new Mock<ISerializationWriterFactory>();
        mockSerializationWriterFactory.Setup(factory => factory.GetSerializationWriter(It.IsAny<string>()))
            .Returns((string _) => new JsonSerializationWriter());

        var mockRequestAdapter = new Mock<IRequestAdapter>(mockBehavior);
        // The first path element must have four characters to mimic v1.0 or beta
        // This is especially needed to mock batch requests.
        mockRequestAdapter.SetupGet(adapter => adapter.BaseUrl).Returns("http://graph.test.internal/mock");
        mockRequestAdapter.SetupSet(adapter => adapter.BaseUrl = It.IsAny<string>());
        mockRequestAdapter.Setup(adapter => adapter.EnableBackingStore(It.IsAny<IBackingStoreFactory>()));
        mockRequestAdapter.SetupGet(adapter => adapter.SerializationWriterFactory).Returns(mockSerializationWriterFactory.Object);

        return mockRequestAdapter;
    }
}

var mockRequestAdapter = RequestAdapterMockFactory.Create();
var graphServiceClient = new GraphServiceClient(mockRequestAdapter.Object);

mockRequestAdapter.Setup(adapter => adapter.SendAsync(
    // Needs to be correct HTTP Method of the desired method πŸ‘‡πŸ»
    It.Is<RequestInformation>(info => info.HttpMethod == Method.GET),
    // Needs to be method from πŸ‘‡πŸ» object type that will be returned from the SDK method.
    Microsoft.Graph.Models.User.CreateFromDiscriminatorValue,
    It.IsAny<Dictionary<string, ParsableFactory<IParsable>>>(), It.IsAny<CancellationToken>()))
    .ReturnsAsync(new Microsoft.Graph.Models.User
    {
        DisplayName = "Hello World",
    });

This approach is quite bloatly, but I didn't find any better for my cases. In the above linked GitHub issue you can find other approaches which maybe works better for you.

Access and use values in ReturnsAsync()

This can be achieved by using the overload of the method that takes a corresponding Func<>. In our case to e.g. access the desired id of a group request and return a group with this id, you could try the following:

mockRequestAdapter.Setup(adapter => adapter.SendAsync(
    // Needs to be correct HTTP Method of the desired method πŸ‘‡πŸ»
    It.Is<RequestInformation>(info => info.HttpMethod == Method.GET),
    // πŸ‘‡πŸ» Needs to be method from object type that will be returned from the SDK method.
    Group.CreateFromDiscriminatorValue,
    It.IsAny<Dictionary<string, ParsableFactory<IParsable>>>(), It.IsAny<CancellationToken>()))
    .ReturnsAsync((
        RequestInformation info,
        // Needs to be the πŸ‘‡πŸ» same type as above!
        ParsableFactory<Group> _,
        Dictionary<string, ParsableFactory<IParsable>> _,
        CancellationToken _) =>
    {
        var id = (string)info.PathParameters["group%2Did"];
        return new Group { Id = id };
    });

Whatever group you're now going to request, you'll get it back with the desired id:

var id = Guid.NewGuid().ToString();
var group = await graphServiceClient.Groups[id].GetAsync();

Graph v4 SDK

Depending on your use case and existing code base you can also provide some empty stubs for both interfaces in the constructor call and use the ability to override the virtual functions. This comes in handy if you use some mock framework like Moq as provided within the documentation:

// Arrange
var mockAuthProvider = new Mock<IAuthenticationProvider>();
var mockHttpProvider = new Mock<IHttpProvider>();
var mockGraphClient = new Mock<GraphServiceClient>(mockAuthProvider.Object, mockHttpProvider.Object);

ManagedDevice md = new ManagedDevice
{
    Id = "1",
    DeviceCategory = new DeviceCategory()
    {
        Description = "Sample Description"
    }
};

// setup the call
mockGraphClient
    .Setup(g => g.DeviceManagement.ManagedDevices["1"]
        .Request()
        .GetAsync(CancellationToken.None))
        .ReturnsAsync(md)
        .Verifiable();

// Act
var graphClient = mockGraphClient.Object;
var device = await graphClient.DeviceManagement.ManagedDevices["1"]
    .Request()
    .GetAsync(CancellationToken.None);

// Assert
Assert.Equal("1",device.Id);

By using this approach you don't have to hassle around with the concrete HTTP request done on the wire. Instead you simple override (nested) method calls with their parameters and define the returned object without a serialization / deserialization step. Also be aware, that within mock you can use e.g. It.IsAny<string>() and similar constructs to define if you need an exact parameter check or something else.

Firewarden answered 2/11, 2021 at 11:53 Comment(1)
thank you! your setup for SerializationWriterFactory is all I needed – Chacma
E
3

I tried using the first suggested solution to this issue, but that does not work with Microsoft.Graph 5.11.0, as Moq cannot override the methods on the GraphServiceClient in that version, but I managed to achieve similar behavior by mocking IRequestAdapter instead.

// Arrange
var mockBaseUrl = "";
var mockRequestAdapter = new Mock<IRequestAdapter>();
var mockGraphServiceClient = new Mock<GraphServiceClient>(
    mockRequestAdapter.Object,
    mockBaseUrl
);

mockRequestAdapter
    .Setup(
        a =>
            a.SendAsync(
                It.Is<RequestInformation>(
                    ri => (string)ri.PathParameters["managedDevice%2Did"] == "1"
                ),
                It.IsAny<ParsableFactory<ManagedDevice>>(),
                It.IsAny<Dictionary<string, ParsableFactory<IParsable>>?>(),
                It.IsAny<CancellationToken>()
            )
    )
    .ReturnsAsync(
        new ManagedDevice()
        {
            Id = "1",
            DeviceCategory = new DeviceCategory() { Description = "Sample description" }
        }
    );

// Act
var device = await mockGraphServiceClient.Object.DeviceManagement.ManagedDevices[
    "1"
].GetAsync();

// Assert
Assert.NotNull(device);
Assert.Equal("1", device!.Id);

If you attempt to use this solution, please remember to change the name of the id path parameter to match the type you're requesting. The naming convention is $"{camelCaseTypeName}%2Did".

Encephalogram answered 25/5, 2023 at 7:8 Comment(0)
B
0

I've created the following fixture for Microsoft Graph API SDK v5 with a more generic approach. For my use-case I only needed getting data, but a similar setup can be created for post.

/// <summary>
/// Graph email service fixture.
/// </summary>
internal class GraphEmailServiceFixture
{
    internal readonly Mock<GraphServiceClient> GraphServiceClientMock;
    private readonly Mock<IRequestAdapter> _requestAdapterMock;

    internal GraphEmailServiceFixture()
    {
        _requestAdapterMock = new Mock<IRequestAdapter>();

        GraphServiceClientMock = new Mock<GraphServiceClient>(_requestAdapterMock.Object, It.IsAny<string>());
    }

    public IGraphEmailService CreateSut() => new GraphEmailService(
        GraphServiceClientMock.Object);

    /// <summary>
    /// Sets up a mock for SendAsync on the GraphEmailService.RequestAdapter, which returns a GraphServiceClient with response.
    /// </summary>
    /// <param name="response">Graph API response.</param>
    /// <returns>The fixture itself - used for chaining when creating fixture.</returns>
    internal GraphEmailServiceFixture WithReturnForGraphClientResponse<ModelType>(ModelType response) where ModelType : IParsable
    {
        _requestAdapterMock
            .Setup(ra => ra.SendAsync(It.IsAny<RequestInformation>(), It.IsAny<ParsableFactory<ModelType>>(), It.IsAny<Dictionary<string, ParsableFactory<IParsable>>>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(response);

        return this;
    }
}

GraphEmailService is the service that contains all methods getting data from the Graph API. It takes one constructor argument for the GraphServiceClient.

Use it as following:

[TestMethod]
public async Task GetMessage_WithAttachments_Expects()
{
    // Arrange.
    var message = new Message();
    var attachments = new AttachmentCollectionResponse
    {
        Value = new List<Attachment>
        {
            new FileAttachment()
        }
    };

    var sut = new GraphEmailServiceFixture()
        .WithReturnForGraphClientResponse(message)
        .WithReturnForGraphClientResponse(attachments)
        .CreateSut();

    // Act.
    var result = await sut.GetMessage();

    // Assert.
    Assert.IsNotNull(result);
    Assert.AreEqual(output, result);
}
Blackshear answered 12/9, 2023 at 9:21 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.