Написание макетов для делегатов в функции Azure с использованием Moq

#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>()));