Open Steve887 opened 1 year ago
Thanks for raising this @Steve887
To help understand this better. Do you wish to be able to mock the invocation of the PatchAsync
method? And that the mocking of the IRequestAdapter interface that all methods use would not be suitable in your scenario?
It's more general than that. Mocking IRequestAdapter seems quite difficult and unintuitive. It would be better to mock the Patch or Get directly.
For example, if I mocked IRequestAdapter how would I Assert that Patch was called? Or set a specific return value for the Get?
I face a similar issue with Mock using IRequestAdapter, it doesn't work for 2nd invocation of graphclient.
here's method
MockTestCase
In this line:
It.IsAny<ParsableFactory<Microsoft.Graph.Models.User>>()
Microsoft.Graph.Models.User
is model from MSGraph API to return, which can be returned in ReturnAsync
method. However I agree that it will be nice to find better way for this :)
Far from ideal, but I'm using above examples and some data from RequestInformation
to set up my mocks for more specific cases. Otherwise I ran into conflicts with multiple setups that return different values.
RequestAdapterMock.Setup(m => m.SendAsync(
It.Is<RequestInformation>(information => information.HttpMethod == Method.GET
&& information.UrlTemplate.Contains("/users/")
&& information.PathParameters.Values.Contains(user.Id)),
It.IsAny<ParsableFactory<User>>(),
It.IsAny<Dictionary<string,ParsableFactory<IParsable>>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(user);
I matched my specific uses with the Graph API reference and used values from the RequestInformation
object.
I think the GraphServiceClient
should be made more testable though. I don't see the rationale behind removing the virtual methods, this complicates things quite a lot. Of course we can create our own abstractions, but isn't it quite a common use case to want to mock this client?
Currently I'm using this approach. First a helper method for the request adapter:
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;
}
}
And here the setup for the call:
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",
});
As already mentioned by others, this is not easy and makes testing more complicated then before when we had an IGraphServiceClient
, but at least it works.
I always use NSubstitute and xUnit. So I created some samples how you could test. It's not very straight forward but it works! Hopefully it will help some people.
[Fact]
public async Task Microsoft_Graph_All_Users()
{
var requestAdapter = Substitute.For<IRequestAdapter>();
GraphServiceClient graphServiceClient = new GraphServiceClient(requestAdapter);
var usersMock = new UserCollectionResponse()
{
Value = new List<User> {
new User()
{
Id = Guid.NewGuid().ToString(),
GivenName = "John",
Surname = "Doe"
},
new User()
{
Id = Guid.NewGuid().ToString(),
GivenName = "Jane",
Surname = "Doe"
}
}
};
requestAdapter.SendAsync(
Arg.Any<RequestInformation>(),
Arg.Any<ParsableFactory<UserCollectionResponse>>(),
Arg.Any<Dictionary<string, ParsableFactory<IParsable>>>(),
Arg.Any<CancellationToken>())
.ReturnsForAnyArgs(usersMock);
var users = await graphServiceClient.Users.GetAsync();
Assert.Equal(2, users.Value.Count);
}
[Fact]
public async Task Microsoft_Graph_User_By_Id()
{
var requestAdapter = Substitute.For<IRequestAdapter>();
GraphServiceClient graphServiceClient = new GraphServiceClient(requestAdapter);
var johnObjectId = Guid.NewGuid().ToString();
var janeObjectId = Guid.NewGuid().ToString();
var userJohn = new User()
{
Id = johnObjectId,
GivenName = "John",
Surname = "Doe"
};
var userJane = new User()
{
Id = janeObjectId,
GivenName = "Jane",
Surname = "Doe"
};
requestAdapter.SendAsync(
Arg.Is<RequestInformation>(ri => ri.PathParameters.Values.Contains(johnObjectId)),
Arg.Any<ParsableFactory<Microsoft.Graph.Models.User>>(),
Arg.Any<Dictionary<string, ParsableFactory<IParsable>>>(),
Arg.Any<CancellationToken>())
.Returns(userJohn);
requestAdapter.SendAsync(
Arg.Is<RequestInformation>(ri => ri.PathParameters.Values.Contains(janeObjectId)),
Arg.Any<ParsableFactory<Microsoft.Graph.Models.User>>(),
Arg.Any<Dictionary<string, ParsableFactory<IParsable>>>(),
Arg.Any<CancellationToken>())
.Returns(userJane);
var userResultJohn = await graphServiceClient.Users[johnObjectId].GetAsync();
Assert.Equal("John", userResultJohn.GivenName);
var userResultJane = await graphServiceClient.Users[janeObjectId].GetAsync();
Assert.Equal("Jane", userResultJane.GivenName);
}
[Fact]
public async Task Microsoft_Graph_User_By_GivenName()
{
var requestAdapter = Substitute.For<IRequestAdapter>();
GraphServiceClient graphServiceClient = new GraphServiceClient(requestAdapter);
var johnObjectId = Guid.NewGuid().ToString();
var janeObjectId = Guid.NewGuid().ToString();
var userJohn = new User()
{
Id = johnObjectId,
GivenName = "John",
Surname = "Doe"
};
var userJane = new User()
{
Id = janeObjectId,
GivenName = "Jane",
Surname = "Doe"
};
requestAdapter.SendAsync(
Arg.Is<RequestInformation>(ri => ri.QueryParameters["%24filter"].ToString() == "givenName='John'"),
Arg.Any<ParsableFactory<UserCollectionResponse>>(),
Arg.Any<Dictionary<string, ParsableFactory<IParsable>>>(),
Arg.Any<CancellationToken>())
.Returns(new UserCollectionResponse()
{
Value = new List<User>() { userJohn }
});
requestAdapter.SendAsync(
Arg.Is<RequestInformation>(ri => ri.QueryParameters["%24filter"].ToString() == "givenName='Jane'"),
Arg.Any<ParsableFactory<UserCollectionResponse>>(),
Arg.Any<Dictionary<string, ParsableFactory<IParsable>>>(),
Arg.Any<CancellationToken>())
.Returns(new UserCollectionResponse()
{
Value = new List<User>() { userJane }
});
var userResultJohn = await graphServiceClient.Users.GetAsync(rc =>
{
rc.QueryParameters.Filter = "givenName='John'";
});
Assert.NotNull(userResultJohn);
Assert.Equal(1, userResultJohn.Value.Count);
Assert.Equal("John", userResultJohn.Value.First().GivenName);
var userResultJane = await graphServiceClient.Users.GetAsync(rc =>
{
rc.QueryParameters.Filter = "givenName='Jane'";
});
Assert.NotNull(userResultJane);
Assert.Equal(1, userResultJane.Value.Count);
Assert.Equal("Jane", userResultJane.Value.First().GivenName);
}
If you want some guidance on how to write unit tests for in example Post calls and to check the contents of your call. You can follow this thread: https://github.com/microsoft/kiota/issues/2767
Thank you @LockTar your examples really saved me some headaches.
Thanks for all the examples, helped me a lot. One tip I want to share:
mockSerializationWriterFactory.Setup(factory => factory.GetSerializationWriter(It.IsAny<string>()))
.Returns(new JsonSerializationWriter());
should be replaced with
mockSerializationWriterFactory.Setup(factory => factory.GetSerializationWriter(It.IsAny<string>()))
.Returns(() => new JsonSerializationWriter());
in case you want to invoke a PostAsync multiple times. More context can be found here: https://github.com/microsoft/kiota/issues/2767
Hey @LockTar, these examples are great.
Any idea on how to access nested resources and override their response values.
Specifically I am after the user/{userId}/drive endpoint
@compdesigner-nz The good news is for nested resources the request adapter only gets called once for the leaf resource (in your case: Drive
). As you can see in @LockTar 's Microsoft_Graph_User_By_Id
, there's a RequestInformation object that's passed to RequestAdapter that contains all of the IDs of all indexed parent resources.
The following steps describe how to figure out how to mock out this call: graphServiceClient.Users[johnObjectId].Drive
UsersRequestBuilder
.UsersRequestBuilder
, look for the index method. Notice when you index users, the code adds an entry to the PathParameters object where the key is "user%2Did"
and the value is the user id, i.e., johnObjectId
in our example.UserItemRequestBuilder
, so check that out and look for the Drive
property. That gives us a DriveRequestBuilder
.GetAsync()
on, so the return type is Microsoft.Graph.Models.Drive
.aDriveToReturn
is your expected return value for the test):
requestAdapter.SendAsync(
Arg.Is<RequestInformation>(ri => ri.PathParameters.Any((keyValuePair) => keyValuePair.Key == "user%2Did" && keyValuePair.Value == johnObjectId)),
Arg.Any<ParsableFactory<Microsoft.Graph.Models.Drive>>(),
Arg.Any<Dictionary<string, ParsableFactory<IParsable>>>(),
Arg.Any<CancellationToken>())
.Returns(aDriveToReturn);
(replace with mocking library of your choice) Note that we mock SendAsync, since that's what GetAsync()
calls on the RequestAdapter.
Note that I haven't run this, but have some similar code that works. If there are any issues, please post back so I can correct them.
Ah! Fantastic, thanks so much @yagni! That works perfectly ππΌ
Hey @yagni, another question:
What about the /content
endpoint on drives[driveId]/items[itemId]
? It says that this returns a 302 and a preauthenticated URL. I assume the response type is still DriveItem
and that is fed as an argument under Arg.Any<ParsableFactory<DriveItem>>()
?
I see that the /content
endpoint defined under here defines a parameter key of ?%24format*
. I assume I add a new conditional under my ri
fluent expression to check to see if a key in QueryParameters exists with that name? From there it's a matter of overriding the return value...?
Correct?
The format
parameter is only needed, if you like to download a file in a specific format, that maybe differs from the original format. If you don't set this parameter you'll get back the file as-is from the drive. For more details about this auto conversions take a look at the documentation.
Also be aware, depending on your usage of the api, you don't need to explicitly call the /content
endpoint to retrieve a url to the file content. When you request an item via its id you also get back the property @microsoft.graph.downloadUrl
which is the very same url as under content
. For more details take a look at the documentation.
For your mocking (or production) code this means, that you have to take this url and request it via a simple HttpClient
(which you have to mock too) to get the content. Depending on the file size or you needs you can either request the whole file or byte ranges. For more details, take a look at the documentation.
RequestAdapterMock.Setup(m => m.SendAsync( It.Is<RequestInformation>(information => information.HttpMethod == Method.GET && information.UrlTemplate.Contains("/users/") && information.PathParameters.Values.Contains(user.Id)),
Using Moq, I tried this method from @ysbakker but compiler complains about cannot convert from RequestInformation to Action<Microsoft.Graph.Users.Item.UserItemRequestBuilder.UserItemRequestBuilderGetRequestConfiguration>
. So, this was the best I could come up with to get working. This needs to be fixed; unit testing code written using this SDK is a nightmare.
graphServiceClientMock?.Setup(_ => _.Users[It.IsAny<string>()].GetAsync(
It.IsAny<Action<Microsoft.Graph.Users.Item.UserItemRequestBuilder.UserItemRequestBuilderGetRequestConfiguration>>(), default)).ReturnsAsync(user);
It's more general than that. Mocking IRequestAdapter seems quite difficult and unintuitive. It would be better to mock the Patch or Get directly.
For example, if I mocked IRequestAdapter how would I Assert that Patch was called? Or set a specific return value for the Get?
It looks like you'd assert patch using the RequestInformation.HttpMethod provided to your mock. Of course, that's still not exactly intuitive.
RequestAdapterMock.Setup(m => m.SendAsync( It.Is<RequestInformation>(information => information.HttpMethod == Method.GET && information.UrlTemplate.Contains("/users/") && information.PathParameters.Values.Contains(user.Id)),
Using Moq, I tried this method from @ysbakker but compiler complains about cannot convert from RequestInformation to
Action<Microsoft.Graph.Users.Item.UserItemRequestBuilder.UserItemRequestBuilderGetRequestConfiguration>
. So, this was the best I could come up with to get working. This needs to be fixed; unit testing code written using this SDK is a nightmare.graphServiceClientMock?.Setup(_ => _.Users[It.IsAny<string>()].GetAsync( It.IsAny<Action<Microsoft.Graph.Users.Item.UserItemRequestBuilder.UserItemRequestBuilderGetRequestConfiguration>>(), default)).ReturnsAsync(user);
@kfwalther are you sure you're on v5? It looks like you're using the earlier style of mocking, back when GraphServiceClient implemented an interface that had a bunch of virtual methods you could mock.
RequestAdapterMock.Setup(m => m.SendAsync( It.Is<RequestInformation>(information => information.HttpMethod == Method.GET && information.UrlTemplate.Contains("/users/") && information.PathParameters.Values.Contains(user.Id)),
Using Moq, I tried this method from @ysbakker but compiler complains about cannot convert from RequestInformation to
Action<Microsoft.Graph.Users.Item.UserItemRequestBuilder.UserItemRequestBuilderGetRequestConfiguration>
. So, this was the best I could come up with to get working. This needs to be fixed; unit testing code written using this SDK is a nightmare.graphServiceClientMock?.Setup(_ => _.Users[It.IsAny<string>()].GetAsync( It.IsAny<Action<Microsoft.Graph.Users.Item.UserItemRequestBuilder.UserItemRequestBuilderGetRequestConfiguration>>(), default)).ReturnsAsync(user);
There seems to be something wrong elsewhere in your code, this example code works like expected and I added a few comment where most of the time the error you described occurs:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions.Store;
using Microsoft.Kiota.Serialization.Json;
using Moq;
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);
mockRequestAdapter.SetupGet(adapter => adapter.BaseUrl).Returns("http://test.internal");
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;
}
}
public class Program
{
public static async Task Main()
{
var mockRequestAdapter = RequestAdapterMockFactory.Create();
mockRequestAdapter.Setup(adapter => adapter.SendAsync(
It.Is<RequestInformation>(info => info.HttpMethod == Method.GET && info.UrlTemplate.Contains("/groups/")),
// ππ» Must match the type that service client should return
Group.CreateFromDiscriminatorValue,
It.IsAny<Dictionary<string, ParsableFactory<IParsable>>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((
RequestInformation info,
// Must match the ππ» type that service client should return
ParsableFactory<Group> _,
Dictionary<string, ParsableFactory<IParsable>> _,
// Must match the type that ππ» service client should return
CancellationToken _) => new Group
{
Id = (string)info.PathParameters["group%2Did"],
});
var graphClient = new GraphServiceClient(mockRequestAdapter.Object);
var group = await graphClient.Groups["fooBar"].GetAsync();
Console.WriteLine(group.Id);
}
}
Here's another example using FakeItEasy to read and write subscriptions:
var adapter = A.Fake<IRequestAdapter>();
// Read existing subscriptions
var readSubscriptions = A.CallTo(() => adapter.SendAsync(A<RequestInformation>.Ignored,
A<ParsableFactory<SubscriptionCollectionResponse>>.Ignored,
A<Dictionary<string, ParsableFactory<IParsable>>>.Ignored,
A<CancellationToken>.Ignored)
);
readSubscriptions.ReturnsLazily((RequestInformation requesInfo,
ParsableFactory<SubscriptionCollectionResponse> factory,
Dictionary<string, ParsableFactory<IParsable>>? errorMapping,
CancellationToken cancellationToken) =>
{
return Task.FromResult<SubscriptionCollectionResponse?>(new SubscriptionCollectionResponse()
{
Value = []
});
});
// Post new subscription
var createSub = A.CallTo(() => adapter.SendAsync(A<RequestInformation>.Ignored,
A<ParsableFactory<Subscription>>.Ignored,
A<Dictionary<string, ParsableFactory<IParsable>>>.Ignored,
A<CancellationToken>.Ignored)
);
createSub.ReturnsLazily((RequestInformation requesInfo,
ParsableFactory<Subscription> factory,
Dictionary<string, ParsableFactory<IParsable>>? errorMapping,
CancellationToken cancellationToken) =>
{
return Task.FromResult<Subscription?>(new Subscription());
});
var client = new GraphServiceClient(adapter);
V5 of the Azure Graph API has removed the IGraphServiceClient and has methods that are not virtual. This makes mocking calls to the Graph API client extremely difficult as you have to mock the underlying Request Handler code (https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/1667).
Is there a recommended way to Mock calls to the GraphServiceClient in order for me to check my logic calling several Graph API methods.
For example, I have code like:
How would I go about mocking the GetAsync and PatchAsync methods?