Debugging your functions is not enough to guarantee that your code reacts appropriately to all the possible scenarios in which it is involved. Furthermore, you cannot automatically repeat the debug procedures every time part of your code changes.
Before we look at how to implement unit tests for your functions, it is important to clarify some definitions about unit testing.
Unit testing is a particular type of test where individual units/components of your code are tested. The purpose of the unit test is to validate that the behavior of a component is exactly the behavior of your design.
Unit testing is the first type of test that you execute in the software development pipeline, and, generally, it is created and managed by the developer.
Implementing unit tests gives you the following benefits:
- It increases your confidence in changing or maintaining code. You can refactor or change your code with no worries because if the tests you have prepared give a positive result after refactoring or changing the code, it means that the behavior of the software has not changed.
- Your code is more reusable because if you want to make unit tests, you have to write code that is more modular, so it will probably be more reusable.
- Fixing a bug during unit testing (during the first phase of your test, when your code is not in production yet) is cheaper than fixing a bug when your code is in production.
But unit tests also have the following disadvantages:
- Using the unit test approach, you will be able to capture errors caused by the code present in the code unit being tested, but it is not easy to find errors that are caused by the interaction of different components (you must use integration tests for this purpose)
- In complex software, it is practically impossible to write a test for every possible scenario envisaged by your code unit
Of course, implementing unit tests is not simple, and you have to design your code to support them.
In the real world, your code will include references to external objects (for example, your methods could call external services or write to a database). In order to be unit testable, your code should be able to replace external references with components whose behavior is known. This is because, otherwise, your test will not only run on your code, but also on the code of external references, and this would mean that the code being tested is no longer unitary. So instead of these external resources, the code being tested uses friendly components. These friendly components are called mocks or stubs, and the way to use them is by using a mock library that helps you to create the mock components in a declarative way.
In the example snippet of this section, we will use the Moq library to implement these mocks, but, of course, you can use your favorite mock library.
If you look at one Azure Function signature, you can find the following dependencies:
- Trigger: As you saw in the previous sections, the function trigger is implemented by an attribute and a payload (generally a simple class that contains the trigger data).
- Bindings: The bindings are also implemented with an attribute and, generally, a class or an interface that models the interaction with the binding source.
- Logger: The logger is passed to the function using the ILogger interface.
To simplify, let's suppose we have the following function, which calculates a mortgage loan:
[FunctionName(FunctionNames.MortgageCalculatorFunction)]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
[Table("executionsTable", Connection = "StorageAccount")] ICollector<ExecutionRow> outputTable,
ILogger log)
{
// Code to retrieve info from the request and calculate the loan
}
The preceding code contains the following elements:
- The req parameter is an instance of HttpRequest and contains the information of the HttpTrigger (defined by the HttpTriggerAttribute attribute).
- The outputTable parameter is an instance of ICollector<T>, and it is the handle you can use in the function to interact with the storage table.
- The parameter log is an instance of the ILogger interface, and you can use it to write your log.
When the function runs in the runtime environment, all the parameter instances are managed by the runtime itself, but when you test the function, you can mock them because they are simple classes or instances of an interface.
An example of a unit test for the MortgageCalculatorFunction function you saw previously is as follows:
[Fact]
public async Task Run_RightParametersInQueryString_CalculateTheRate()
{
var request = new Mock<HttpRequest>();
request.Setup(e => e.Query["loan"]).Returns("100000");
request.Setup(e => e.Query["interest"]).Returns("0.06");
request.Setup(e => e.Query["nPayments"]).Returns("180");
var table = new Mock<ICollector<ExecutionRow>>();
table.Setup(t => t.Add(It.IsAny<ExecutionRow>()))
.Verifiable();
var logger = new Mock<ILogger>();
var actual = await MortgageFunctions.Run(request.Object, table.Object, logger.Object);
// Assert
}
The test is written using the xUnit.net test framework, but you can use your favorite testing engine.
The HTTP request, the collector, and the logger are mocked using the Moq library and are passed to the function. Of course, you have to set up all the mock methods that the function will call in the scenario you are testing (that is, in the previous sample, the method to access the query string of the request or the Add method of the ICollector<T>).
Mocking the parameters of the function is relatively simple, but what happens if you need to use your own service within your function? For example, let's suppose that the mortgage calculation algorithm is implemented within an instance of an interface (for example, IMortgageCalculator) and you need to use it inside the Run method. In this case, you cannot add the IMortgageCalculator in the function signature; otherwise, you will have an error (because it is neither a trigger nor a binding). To solve this, you have to use the dependency injection pattern in your Azure Function.