我在我的项目中使用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来模拟上面的代码或任何其他方法来完成测试用例吗?
答案 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();
}
}
由于我们使用自己的 IAuthenticationProvider
和 IHttpProvider
,我们可以根据调用的 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;
}
}