In May at the //Build event, Azure Functions Team announced supporting dependency injection feature. This was one of the long-waited features on Azure Functions, and we don’t have to use some dodge way for dependency injections any longer. I wrote a blog post about this in a few months back.
However, we still need more testing bits and pieces on Azure Functions like API endpoint testing that includes integration testing and external API connectivity testing. How can we achieve this, without having to deploy Azure Functions instance onto Azure? There are a few API mocking tools to make use of, and Mountebank is one of them. Throughout this post, I’m going to discuss an integration testing strategy with Mountebank, in our local development environment.
You can find the sample codes used in this post at this GitHub repository.
System High-level Architecture
In this post, I’m going to develop an API application using Azure Functions. This API calls an external API to process data. As its first step, I’m going to create a health-check endpoint to validate API availability, as well as the external API’s availability. Here’s the high-level diagram describing this API application.
My Azure Function API has the endpoint of
https://fncapp-mountebank/api/ping, which calls the external API endpoint of
https://fncapp-one-api/api/ping to confirm its availability.
First of all, let’s create our API health-check endpoint,
HealthCheckHttpTrigger. As it doesn’t have much business logic, we can write the code like below. For clarity, I only included the core logic here.
This trigger only calls the
HealthCheckFunction class that contains all the business logic. This trigger sends a request to the external API through the
HttpClient instance and returns a response based on the API request.
If you’re interested in the
FunctionBase<ILogger>class or the
IFunction<ILogger>interface, they’re from Aliencube.AzureFunctions.Extensions.DependencyInjection package.
So, we’ve got the endpoint for the health-check. Now, let’s move onto the testing logic.
Writing Unit Tests
In the sample codes, there are more than one tests, but I’m just putting only one unit-test code as an example.
As you can see above, I mocked the
IHealthCheckFunction interface to control dependencies. The trigger doesn’t need to know how
HealthCheckFunction works but is only interested in what it returns. Therefore mocking the interface like this is very common.
HealthCheckFunction is the main player of my Azure Functions API. It contains the logic that directly talks to the external API. For convenience, I put only one test case here.
Although we need to talk to the external API, at the unit-testing level, we don’t need to worry about the external service connectivity. Instead, we also can mock the behaviour like above. I put the
FakeMessageHandler instance into the instance of
HttpClient to mock its response. Now we’ve got complete isolated unit-test cases. Let’s run them.
And we’ve got the successful test result.
If you’re curious of the option,
--filter:"TestCategory!=Integration&TestCategory!=E2E", I’ll explain it later in this post.
Writing Integration Tests
Unlike unit-tests that runs without connectivity to external resources, integration-tests need connectivity. In other words, the test codes should have control over the connection to external resources. While unit-tests manipulates the results from the external resources by mocking the codes, integration-tests doesn’t change the code but manipulates the responses from the external API. Therefore, I should be able to call my Azure Functions API endpoint and mock the external API response payloads. It requires a few additional steps beforehand, by the way.
And to run Mountebank, execute the following command.
If you want to know more about Mountebank, read this page – getting started. Fortunately, there’s the .NET wrapper called, MbDotNet. So, we can run Mountebank within our integration-testing code. Comprehensive usage can be found at this document.
Writing Integration Tests
Integration-testing code is relatively simple, comparing to the unit-testing codes because there’s no mocking at the code level. We can mock the API response payload, using
MbDotNet Here’s the entire testing codes:
As you can see above, the test code calls the Azure Functions API endpoint, not external API. We should pay attention to the
MountebankServerFixture class. Let’s have a look.
In the fixture class, the
GetHealthCheckUrl method mocks the API responses and returns the Azure Functions API endpoint. You may notice that the integration-test method diverts the endpoint to the external API to the mocked API endpoint like the image below:
Running Integration Tests
The test code is ready. It’s time to run the integration tests. To run the tests, the Azure Functions must be up and running, and so must the Mountebank server. Run the following command to run both Mountebank and Azure Functions instance in the local environment:
The command above runs both Mountebank and Azure Functions runtime in the background. If you want to run both in their console respectively, open two consoles and run the command in each console.
Once running both services in separate console windows, here’s the screenshot:
All preps are done. Let’s run the integration tests. Open another console window and run the following command for the tests.
You may pick up the option,
--filter:"TestCategory=Integration". We put the
TestCategory decorator on a few test methods. With this filter, we only execute test methods having the category of
Integration. Once they’re run, we’ll be able to see the screen below:
The video clip below shows all the tests running without being filtered out.
So far, we’ve discussed a few ways to run unit tests and integration tests in our local development environment. For integration testing, we need to have the environment ready for API mocking and run the Azure Function instance before the tests run.
The post shows the implementation first, followed by test codes. It might give you a different impression of why test codes are written later. But this depends on your approach. If you prefer either TDD or BDD, test codes come first, or at least at the same time of writing logics.
In the next post, I’m going to discuss running Azure Functions end-to-end tests and how it’s possible in the Azure DevOps pipelines.