#azure #unit-testing #azure-functions #moq #xunit
#лазурный #модульное тестирование #azure-функции #мок #xunit
Вопрос:
У меня есть функция Azure, которая в основном вызывается при HttpRequest к конечной точке. Затем эта функция выполняет вызовы соответствующих разделов в базе данных на основе сообщения CREATE или UPDATE, которое передается в полезной нагрузке.
public class InboundEvent
{
private readonly Func<MessageType, IMessageProcessor> _serviceProvider;
private readonly IAccessTokenValidator _accessTokenValidator;
public InboundEvent(Func<MessageType, IMessageProcessor> serviceProvider, IAccessTokenValidator accessTokenValidator)
{
_serviceProvider = serviceProvider;
_accessTokenValidator = accessTokenValidator;
}
[FunctionName("InboundEvent")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "abc/input")] HttpRequest req,
ILogger log)
{
try
{
await _accessTokenValidator.ValidateToken(req);
log?.LogInformation($"InboundMessageProcessor executed at: {DateTime.UtcNow}");
var request = await ProcessRequest(req);
await _serviceProvider(request.MessageType).ProcessMessage(request);
log?.LogInformation("InboundMessageProcessor function executed successfully.");
return new OkObjectResult("OK");
}
catch (Exception ex)
{
log?.Log(LogLevel.Error, ex, "Error");
return new InternalServerErrorResult();
}
}
private async Task<InputModel> ProcessRequest(HttpRequest req)
{
InputModel messageReq = new InputModel();
if (req.Headers != null amp;amp; req.Headers.Any())
{
// form the InputModel datamodel object
// basically it can contain CREATE or UPDATE information
}
messageReq.Message = await new StreamReader(req.Body).ReadToEndAsync();
return messageReq;
}
}
Где компонент ServiceProvider вызывает MainBookingProcessor, который выполняет соответствующие действия на основе типа транзакции «СОЗДАТЬ» или «ОБНОВИТЬ»
public class MainBookingProcessor : IMessageProcessor
{
private readonly ICommandDispatcher _commandDispatcher;
public MainBookingProcessor(ICommandDispatcher commandDispatcher)
{
_commandDispatcher = commandDispatcher ?? throw new ArgumentNullException(nameof(commandDispatcher));
}
public async Task ProcessMessage(InputModel req)
{
switch (req.TransactionType)
{
case TransactionType.CREATE:
var command = new CreateBookingCommand()
{
System = req.System,
MessageId = req.MessageId,
Message = req.Message
};
await _commandDispatcher.SendAsync(command);
break;
case TransactionType.UPDATE:
var updateCommand = new UpdateBookingCommand()
{
System = req.System,
MessageId = req.MessageId,
Message = req.Message
};
await _commandDispatcher.SendAsync(updateCommand);
break;
default:
throw new KeyNotFoundException();
}
}
}
Теперь начинается основная часть проблемы, с которой я столкнулся. Я пишу тестовый компонент для тестирования этой функции Azure с использованием xUnit и Moq. Для этого я создал класс InboundEventTests, который будет содержать тестовые методы для тестирования метода Run функции InboundEvent Azure
public class InboundEventTests : FunctionTest
{
private InboundEvent _sut;
private readonly Mock<IMessageProcessor> messageProcessorMock
= new Mock<IMessageProcessor>();
private readonly Mock<Func<MessageType, IMessageProcessor>> _serviceProviderMock
= new Mock<Func<MessageType, IMessageProcessor>>();
private readonly Mock<IAccessTokenValidator> _accessTokenValidator
= new Mock<IAccessTokenValidator>();
private readonly Mock<ILogger> _loggerMock = new Mock<ILogger>();
private HttpContext httpContextMock;
private HeaderDictionary _headers;
private Mock<InputModel> inputModelMock = new Mock<InputModel>();
public InboundEventTests()
{
inputModelMock.SetupProperty(x => x.Message, It.IsAny<string>());
inputModelMock.SetupProperty(x => x.MessageId, It.IsAny<Guid>());
inputModelMock.SetupProperty(x => x.System, It.IsAny<string>());
}
public HttpRequest HttpRequestSetup(Dictionary<String, StringValues> query, string body)
{
var reqMock = new Mock<HttpRequest>();
reqMock.Setup(req => req.Headers).Returns(new HeaderDictionary(query));
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(body);
writer.Flush();
stream.Position = 0;
reqMock.Setup(req => req.Body).Returns(stream);
return reqMock.Object;
}
private HeaderDictionary CreateHeaders()
{
_headers = new HeaderDictionary();
_headers.TryAdd("MessageType","BOOKING");
_headers.TryAdd("TransactionType", "UPDATE");
_headers.TryAdd("MessageId", "some guid");
_headers.TryAdd("System", "NSCP_ORDER_MANAGEMENT");
return _headers;
}
[Fact]
public async Task RunFunctionTest()
{
//Arrange
var query = new Dictionary<String, StringValues>();
query.TryAdd("MessageType", "BOOKING");
query.TryAdd("TransactionType", "UPDATE");
query.TryAdd("System", "ORDER_MANAGEMENT");
query.TryAdd("MessageId", "some guid");
var body = JsonSerializer.Serialize(new {
Message = "BOOKING",
System = "ORDER_MANAGEMENT",
MessageId = "some guid"
});
Место, где я застрял, — это создание макетов для функции делегирования <MessageType,IMessageProcessor>, которая по сути направляет к определенному классу и транзакции. Как я могу написать фиктивные заглушки,
чтобы я мог передавать эти фиктивные объекты в мою тестируемую систему и проверять правильность, если она была вызвана правильно, тем самым отправляя статус.OK как результат
_sut = new InboundEvent(_serviceProviderMock.Object, _accessTokenValidator.Object);
var result = await _sut.Run(req: HttpRequestSetup(query, body), _loggerMock.Object);
var resultObject = (OkObjectResult)resu<
//Assert
Assert.Equal("OK", resultObject.Value);
Вещи, которые я пробовал:
Однако создание макета делегата с использованием приведенного ниже синтаксиса
Mock<Func<MessageType, IMessageProcessor>> _serviceProviderMock = new Mock<Func<MessageType, IMessageProcessor>>();
_serviceProviderMock.Setup(_ => _(It.IsAny<MessageType>())).Returns(It.IsAny<IMessageProcessor>());
_sut = new InboundEvent(_serviceProviderMock.Object, _accessTokenValidator.Object);
var result = await _sut.Run(req: HttpRequestSetup(query, body), _loggerMock.Object);
Но все же ProcessMessage в классе InboundEvent завершается ошибкой, ссылка на объект не установлена для экземпляра, поскольку данные равны нулю.
Ответ №1:
Если InputModel
это POCO без побочных эффектов, тогда нет необходимости издеваться над ним. Просто создайте экземпляр и используйте его.
Нет необходимости использовать Moq для издевательства над делегатом. Создайте делегата, который будет вести себя так, как требуется для теста, и используйте его
//...
Func<MessageType, IMessageProcessor> _serviceProviderMock = messageType => {
//messageType can be inspected and a result returned as needed
return messageProcessorMock.Object;
};
_sut = new InboundEvent(_serviceProviderMock, _accessTokenValidator.Object);
//...
Но как я могу проверить, что делегат вызван
Вы можете поместить логический флаг в делегат и утверждать, что
boolean delegateInvoked = false;
Func<MessageType, IMessageProcessor> _serviceProviderMock = messageType => {
delegateInvoked = true;
//messageType can be inspected and a result returned as needed
return messageProcessorMock.Object;
};
// ...
// Assert if delegateInvoked is true
или, если вызывается издевательский процессор, то это по расширению будет означать, что делегат был вызван, возвращая указанный издевательский процессор.
messageProcessorMock.Verify(_ => _.ProcessMessage(It.IsAny<InputModel>()));