In this post, we will see how to create Unit Test for an Azure Function named “GetListInt”, using Moq, xUnit, and Fluent Assertions.
This function is triggered by HTTP requests and takes two query parameters: to and Type.
Based on these inputs, it generates a list of integers that are either odd or even, depending on the specified type. The function includes validation to ensure the inputs are correct and provides error messages when they are not.
Additionally, we will create the same Azure Function triggered by “Azure Service Bus”.
This variation will listen for messages on a Service Bus queue, process the same input generating the same list of integers.
Before diving into the tests, let’s review the tools we will use:
Moq: it allows us to simulate dependencies so we can test only the code in question.
xUnit: is a testing framework for .NET.
Fluent Assertions: Fluent Assertions is a library that enhances test readability.
Azure Function – HTTP Trigger:
[Azure Function]
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Testing.AzureFunction;
public class GetListInt
{
private readonly ILogger<GetListInt> _logger;
// Constructor to inject the logger dependency
public GetListInt(ILogger<GetListInt> logger)
{
_logger = logger;
}
// Azure Function triggered by an HTTP GET request
[Function("GetListInt")]
public IActionResult GetList([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req)
{
List<int> lstResult = new List<int>();
var toInput = req.Query["to"]; // Retrieve 'to' query parameter
var typeInput = req.Query["type"]; // Retrieve 'type' query parameter
// Validate that both query parameters are provided
if (string.IsNullOrEmpty(toInput) || string.IsNullOrEmpty(typeInput))
{
return new BadRequestObjectResult("Values 'to' and 'type' are required in the query string");
}
// Validate that 'to' is an integer
if (!int.TryParse(toInput, out int toInteger))
{
return new BadRequestObjectResult("The value of 'to' must be an integer.");
}
// Validate that 'to' is greater than 2
if (toInteger <= 2)
{
return new BadRequestObjectResult("The value of 'to' must be greater than 2.");
}
// Normalize 'type' parameter for case-insensitive comparison
string toNormalized = typeInput.ToString().ToLower().Trim();
// Validate that 'type' is either "even" or "odd"
if (toNormalized != "odd" && toNormalized != "even")
{
return new BadRequestObjectResult("The value of 'type' must be even or odd.");
}
// Generate the list based on the type
int indexStart = 1;
if (toNormalized == "even")
{
indexStart = 2;
}
// Add numbers to the list
for (int i = indexStart; i < toInteger; i += 2)
{
lstResult.Add(i);
}
// Return the result as a JSON response
return new OkObjectResult($"The list of {toNormalized} numbers from 1 to {toInteger} is [{string.Join(", ", lstResult)}]");
}
}
[Unit Test]
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace Testing.AzureFunction;
public class TestgetList
{
private readonly Mock<ILogger<GetListInt>> _mockLogger;
private readonly GetListInt _function;
public TestgetList()
{
_mockLogger = new Mock<ILogger<GetListInt>>();
_function = new GetListInt(_mockLogger.Object);
}
[Fact]
public void GetList_Should_Return_BadRequest_When_Parameters_Are_Missing()
{
// Arrange: Set up an empty HttpRequest mock
var mockRequest = new Mock<HttpRequest>();
mockRequest.Setup(req => req.Query["to"]).Returns((string)null);
mockRequest.Setup(req => req.Query["type"]).Returns((string)null);
// Act: Invoke the function
var result = _function.GetList(mockRequest.Object);
// Assert: Verify it returns a BadRequestObjectResult
result.Should().BeOfType<BadRequestObjectResult>();
(result as BadRequestObjectResult).Value.Should().Be("Values 'to' and 'type' are required in the query string");
}
[Fact]
public void GetList_Should_Return_BadRequest_When_To_Is_Not_Integer()
{
// Arrange
var mockRequest = new Mock<HttpRequest>();
mockRequest.Setup(req => req.Query["to"]).Returns("abc");
mockRequest.Setup(req => req.Query["type"]).Returns("even");
// Act
var result = _function.GetList(mockRequest.Object);
// Assert
result.Should().BeOfType<BadRequestObjectResult>();
(result as BadRequestObjectResult).Value.Should().Be("The value of 'to' must be an integer.");
}
[Fact]
public void GetList_Should_Return_BadRequest_When_To_Is_Less_Than_Or_Equal_Two()
{
// Arrange
var mockRequest = new Mock<HttpRequest>();
mockRequest.Setup(req => req.Query["to"]).Returns("2");
mockRequest.Setup(req => req.Query["type"]).Returns("odd");
// Act
var result = _function.GetList(mockRequest.Object);
// Assert
result.Should().BeOfType<BadRequestObjectResult>();
(result as BadRequestObjectResult).Value.Should().Be("The value of 'to' must be greater than 2.");
}
[Fact]
public void GetList_Should_Return_BadRequest_When_Type_Is_Invalid()
{
// Arrange
var mockRequest = new Mock<HttpRequest>();
mockRequest.Setup(req => req.Query["to"]).Returns("10");
mockRequest.Setup(req => req.Query["type"]).Returns("prime");
// Act
var result = _function.GetList(mockRequest.Object);
// Assert
result.Should().BeOfType<BadRequestObjectResult>();
(result as BadRequestObjectResult).Value.Should().Be("The value of 'type' must be even or odd.");
}
[Fact]
public void GetList_Should_Return_Even_Numbers_When_Type_Is_Even()
{
// Arrange
var mockRequest = new Mock<HttpRequest>();
mockRequest.Setup(req => req.Query["to"]).Returns("10");
mockRequest.Setup(req => req.Query["type"]).Returns("even");
// Act
var result = _function.GetList(mockRequest.Object);
// Assert
result.Should().BeOfType<OkObjectResult>();
(result as OkObjectResult).Value.Should().Be("The list of even numbers from 1 to 10 is [2, 4, 6, 8]");
}
[Fact]
public void GetList_Should_Return_Odd_Numbers_When_Type_Is_Odd()
{
// Arrange
var mockRequest = new Mock<HttpRequest>();
mockRequest.Setup(req => req.Query["to"]).Returns("10");
mockRequest.Setup(req => req.Query["type"]).Returns("odd");
// Act
var result = _function.GetList(mockRequest.Object);
// Assert
result.Should().BeOfType<OkObjectResult>();
(result as OkObjectResult).Value.Should().Be("The list of odd numbers from 1 to 10 is [1, 3, 5, 7, 9]");
}
}
If we run the tests, this will be the result:
Azure Function – Service Bus Queue Trigger:
[Azure Function]
namespace Testing.AzureFunction;
public class QueryInput
{
public int? ToInput { get; set; }
public string TypeInput { get; set; }
}
using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace Testing.AzureFunction;
public class GetListIntServiceBus
{
private readonly ILogger<GetListIntServiceBus> _logger;
public GetListIntServiceBus(ILogger<GetListIntServiceBus> logger)
{
_logger = logger;
}
[Function(nameof(GetListIntServiceBus))]
public async Task Run(
[ServiceBusTrigger("testqueue", Connection = "ServiceBusConnection")]
ServiceBusReceivedMessage message,
ServiceBusMessageActions messageActions)
{
try
{
List<int> lstResult = new List<int>();
// Deserialize the message body into a User object
QueryInput queryInput = message.Body.ToObjectFromJson<QueryInput>();
if (queryInput == null)
{
_logger.LogWarning("Received null QueryInput object.");
await messageActions.AbandonMessageAsync(message);
return;
}
// Validate that both query parameters are provided
if (!queryInput.ToInput.HasValue || string.IsNullOrEmpty(queryInput.TypeInput))
{
_logger.LogWarning("Values 'to' and 'type' are required in the query string.");
await messageActions.AbandonMessageAsync(message);
return;
}
// Validate that 'to' is greater than 2
if (queryInput.ToInput.Value <= 2)
{
_logger.LogWarning("The value of 'to' must be greater than 2.");
await messageActions.AbandonMessageAsync(message);
return;
}
// Normalize 'type' parameter for case-insensitive comparison
string toNormalized = queryInput.TypeInput.ToLower().Trim();
// Validate that 'type' is either "even" or "odd"
if (toNormalized != "odd" && toNormalized != "even")
{
_logger.LogWarning("The value of 'type' must be even or odd.");
await messageActions.AbandonMessageAsync(message);
return;
}
// Generate the list based on the type
int indexStart = 1;
if (toNormalized == "even")
{
indexStart = 2;
}
// Add numbers to the list
for (int i = indexStart; i < queryInput.ToInput; i += 2)
{
lstResult.Add(i);
}
_logger.LogInformation($"The list of {toNormalized} numbers from 1 to {queryInput.ToInput.Value} is [{string.Join(", ", lstResult)}]");
// Complete the message
await messageActions.CompleteMessageAsync(message);
}
catch (Exception e)
{
_logger.LogError(e, "Error to manage the 'test' queue ID:{Id}", message.MessageId);
await messageActions.DeadLetterMessageAsync(message);
}
}
}
[UNIT TEST]
using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace Testing.AzureFunction;
public class TestGetListIntServiceBus
{
private readonly Mock<ILogger<GetListIntServiceBus>> _mockLogger;
private readonly Mock<ServiceBusMessageActions> _mockMessageActions;
private readonly GetListIntServiceBus _function;
// Constructor to initialize the test class
public TestGetListIntServiceBus()
{
_mockLogger = new Mock<ILogger<GetListIntServiceBus>>();
_mockMessageActions = new Mock<ServiceBusMessageActions>();
_function = new GetListIntServiceBus(_mockLogger.Object);
}
[Fact]
public async Task Run_Should_AbandonMessage_When_QueryInput_Is_Null()
{
// Arrange: Create a mock Service Bus message with null body
var mockMessage = ServiceBusModelFactory.ServiceBusReceivedMessage(
body: BinaryData.FromObjectAsJson((QueryInput)null)
);
// Act: Call the function
await _function.GetList(mockMessage, _mockMessageActions.Object);
// Assert: Verify AbandonMessageAsync was called
_mockMessageActions.Verify(
actions => actions.AbandonMessageAsync(mockMessage, null, default),
Times.Once
);
// Verify the logger warning was logged
_mockLogger.Verify(
logger => logger.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Received null QueryInput object.")),
null,
It.IsAny<Func<It.IsAnyType, Exception, string>>()
),
Times.Once
);
}
[Fact]
public async Task Run_Should_AbandonMessage_When_Parameters_Are_Missing()
{
// Arrange: Create a mock Service Bus message with missing parameters
var queryInput = new QueryInput { ToInput = null, TypeInput = null };
var mockMessage = ServiceBusModelFactory.ServiceBusReceivedMessage(
body: BinaryData.FromObjectAsJson(queryInput)
);
// Act: Call the function
await _function.GetList(mockMessage, _mockMessageActions.Object);
// Assert: Verify AbandonMessageAsync was called
_mockMessageActions.Verify(
actions => actions.AbandonMessageAsync(mockMessage, null, default),
Times.Once
);
// Verify the logger warning was logged
_mockLogger.Verify(
logger => logger.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Values 'to' and 'type' are required in the query string.")),
null,
It.IsAny<Func<It.IsAnyType, Exception, string>>()
),
Times.Once
);
}
[Fact]
public async Task Run_Should_AbandonMessage_When_ToInput_Is_Less_Than_Or_Equal_Two()
{
// Arrange: Create a mock Service Bus message with invalid 'to' value
var queryInput = new QueryInput { ToInput = 2, TypeInput = "odd" };
var mockMessage = ServiceBusModelFactory.ServiceBusReceivedMessage(
body: BinaryData.FromObjectAsJson(queryInput)
);
// Act: Call the function
await _function.GetList(mockMessage, _mockMessageActions.Object);
// Assert: Verify AbandonMessageAsync was called
_mockMessageActions.Verify(
actions => actions.AbandonMessageAsync(mockMessage, null, default),
Times.Once
);
// Verify the logger warning was logged
_mockLogger.Verify(
logger => logger.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("The value of 'to' must be greater than 2.")),
null,
It.IsAny<Func<It.IsAnyType, Exception, string>>()
),
Times.Once
);
}
[Fact]
public async Task Run_Should_AbandonMessage_When_TypeInput_Is_Invalid()
{
// Arrange: Create a mock Service Bus message with invalid 'TypeInput' value
var queryInput = new QueryInput { ToInput = 3, TypeInput = "odder" };
var mockMessage = ServiceBusModelFactory.ServiceBusReceivedMessage(
body: BinaryData.FromObjectAsJson(queryInput)
);
// Act: Call the function
await _function.GetList(mockMessage, _mockMessageActions.Object);
// Assert: Verify AbandonMessageAsync was called
_mockMessageActions.Verify(
actions => actions.AbandonMessageAsync(mockMessage, null, default),
Times.Once
);
// Verify the logger warning was logged
_mockLogger.Verify(
logger => logger.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("The value of 'type' must be even or odd.")),
null,
It.IsAny<Func<It.IsAnyType, Exception, string>>()
),
Times.Once
);
}
[Fact]
public async Task Run_Should_CompleteMessage_When_Valid_Input_Provided()
{
// Arrange: Create a mock Service Bus message with valid input
var queryInput = new QueryInput { ToInput = 10, TypeInput = "even" };
var mockMessage = ServiceBusModelFactory.ServiceBusReceivedMessage(
body: BinaryData.FromObjectAsJson(queryInput)
);
// Act: Call the function
await _function.GetList(mockMessage, _mockMessageActions.Object);
// Assert: Verify CompleteMessageAsync was called
_mockMessageActions.Verify(
actions => actions.CompleteMessageAsync(mockMessage, default),
Times.Once
);
// Verify the logger information was logged
_mockLogger.Verify(
logger => logger.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("The list of even numbers from 1 to 10 is [2, 4, 6, 8]")),
null,
It.IsAny<Func<It.IsAnyType, Exception, string>>()
),
Times.Once
);
}
}
If we run the tests, this will be the result: