我在c#中使用Moq和xUnit编写单元测试时遇到了问题。
在我的服务中,我有以下代码:
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();
但是我不知道模拟GraphClient
函数的方法:
graphClient.Users.Request().Filter($"displayName eq '{mobilePhone}'").GetAsync()).FirstOrDefault();
Graph v5 SDK
在将Graph SDK升级到v5之后,关于如何模拟服务客户端的方式已经改变,并且没有变得更容易。有不同的可用方法,本期中列出了其中的一些方法。我现在的嘲讽方式是这样的:
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(new JsonSerializationWriter());
var mockRequestAdapter = new Mock<IRequestAdapter>(mockBehavior);
mockRequestAdapter.SetupGet(adapter => adapter.BaseUrl).Returns((string?)null);
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(
It.Is<RequestInformation>(info => info.HttpMethod == Method.GET),
Microsoft.Graph.Models.User.CreateFromDiscriminatorValue,
It.IsAny<Dictionary<string, ParsableFactory<IParsable>>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Microsoft.Graph.Models.User
{
DisplayName = "Hello World",
});
这个方法很臃肿,但是我没有找到更好的方法。在上面链接的GitHub问题中,您可以找到其他可能更适合您的方法。
图v4 SDK
根据您的用例和现有的代码库,您还可以在构造函数调用中为两个接口提供一些空存根,并使用覆盖虚函数的能力。如果您使用文档中提供的mock框架(如Moq),这将派上用场:
// 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);
通过使用这种方法,您不必为在线上完成的具体HTTP请求而烦恼。相反,您只需使用参数覆盖(嵌套)方法调用,并定义返回的对象,而无需序列化/反序列化步骤。还要注意,在mock中,您可以使用例如It.IsAny<string>()
和类似的结构来定义是否需要进行精确的参数检查或其他操作。
我尝试使用第一个建议的解决方案来解决这个问题,但这对微软不起作用。图5.11.0,因为Moq不能覆盖GraphServiceClient
上的方法在该版本中,但我设法通过模拟IRequestAdapter
来实现类似的行为。
// 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);
如果您尝试使用此解决方案,请记住更改id路径参数的名称以匹配您请求的类型。命名约定为$"{camelCaseTypeName}%2Did"
。
我用一种更通用的方法为Microsoft Graph API SDK v5创建了以下fixture。对于我的用例,我只需要获取数据,但是可以为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
是包含从图API获取数据的所有方法的服务。对于GraphServiceClient
,它接受一个构造函数参数。
使用方法如下:
[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);
}