10 min read

Azure Functions Integration Testing with Mountebank

Justin Yoo

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.

Implementing Endpoints

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.

public class HealthCheckHttpTrigger
{
...
// Dependency injections here
[FunctionName(nameof(HealthCheckHttpTrigger.PingAsync))]
public async Task<IActionResult> PingAsync(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "ping")] HttpRequest req,
ILogger log)
{
return result = await this._function
.InvokeAsync<HttpRequest, IActionResult>(req)
.ConfigureAwait(false);
}
}

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.

public interface IHealthCheckFunction : IFunction<ILogger>
{
}
public class HealthCheckFunction : FunctionBase<ILogger>, IHealthCheckFunction
{
...
// Dependency injections here
public override async Task<TOutput> InvokeAsync<TInput, TOutput>(
TInput input,
functionOptionsBase options = null)
{
var result = (IActionResult)null;
var requestUri = $"{this._settings.BaseUri.TrimEnd('/')}/{this._settings.Endpoints.HealthCheck.TrimStart('/')}";
using (var response = await this._httpClient.GetAsync(requestUri).ConfigureAwaitfalse))
{
try
{
response.EnsureSuccessStatusCode();
result = new OkResult();
}
catch (Exception ex)
{
var error = new ErrorResponse(ex);
result = new ObjectResult(error) { StatusCode = (int)response.StatusCode };
}
}
return (TOutput)result;
}
}

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

I've got both HealthCheckHttpTriggerTests and HealthCheckFunctionTests to unit-test against HealthCheckHttpTrigger and HealthCheckFunction respectively. I also used the MSTest framework.

HealthCheckHttpTriggerTests

In the sample codes, there are more than one tests, but I'm just putting only one unit-test code as an example.

[TestMethod]
public async Task Given_Parameters_When_Invoked_Then_InvokeAsync_Should_Return_Result()
{
// Arrange
var result = new OkResult();
var function = new Mock<IHealthCheckFunction>();
function.Setup(p => p.InvokeAsync<HttpRequest, IActionResult>(It.IsAny<HttpRequest>(), It.IsAny<FunctionOptionsBase>()))
.ReturnsAsync(result);
var trigger = new HealthCheckHttpTrigger(function.Object);
var req = new Mock<HttpRequest>();
var log = new Mock<ILogger>();
// Action
var response = await trigger.PingAsync(req.Object, log.Object).ConfigureAwait(false);
// Assert
response
.Should().BeOfType<OkResult>()
.And.Subject.As<OkResult>()
.StatusCode.Should().Be((int)HttpStatusCode.OK);
}

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.

HealthCheckFunctionTests

Now, 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.

[TestMethod]
public async Task Given_Parameters_When_Invoked_Then_InvokeAsync_Should_Return_Result()
{
// Arrange
var result = new OkResult();
var function = new Mock<IHealthCheckFunction>();
function.Setup(p => p.InvokeAsync<HttpRequest, IActionResult>(It.IsAny<HttpRequest>(), It.IsAny<FunctionOptionsBase>()))
.ReturnsAsync(result);
var trigger = new HealthCheckHttpTrigger(function.Object);
var req = new Mock<HttpRequest>();
var log = new Mock<ILogger>();
// Action
var response = await trigger.PingAsync(req.Object, log.Object).ConfigureAwait(false);
// Assert
response
.Should().BeOfType<OkResult>()
.And.Subject.As<OkResult>()
.StatusCode.Should().Be((int)HttpStatusCode.OK);
}

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.

dotnet test [Test_Project_Name].csproj -c Release

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.

Mountebank Setup

Mountebank is a cross-platform API mocking tool and installed via npm. To install, run the following command.

npm install -g mountebank

And to run Mountebank, execute the following command.

mb

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: HealthCheckHttpTriggerTests.

[TestClass]
public class HealthCheckHttpTriggerTests
{
private const string CategoryIntegration = "Integration";
private ServerFixture _fixture;
[TestInitialize]
public void Init()
{
this._fixture = new LocalhostServerFixture();
}
[TestMethod]
[TestCategory(CategoryIntegration)]
public async Task Given_Url_When_Invoked_Then_Trigger_Should_Return_Healthy()
{
// Arrange
var uri = this._fixture.GetHealthCheckUrl();
using (var http = new HttpClient())
// Act
using (var res = await http.GetAsync(uri))
{
// Assert
res.StatusCode.Should().Be(HttpStatusCode.OK);
}
}
[TestMethod]
[TestCategory(CategoryIntegration)]
public async Task Given_Url_When_Invoked_Then_Trigger_Should_Return_Unhealthy()
{
// Arrange
var uri = this._fixture.GetHealthCheckUrl(HttpStatusCode.InternalServerError);
using (var http = new HttpClient())
// Act
using (var res = await http.GetAsync(uri))
{
// Assert
res.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
}
}
}

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.

public class LocalhostServerFixture
{
private readonly MountebankClient _client;
public MountebankServerFixture()
{
this._client = new MountebankClient();
}
public string GetHealthCheckUrl(HttpStatusCode statusCode = HttpStatusCode.OK)
{
this._client.DeleteImposter(8080);
var imposter = this._client
.CreateHttpImposter(8080, statusCode.ToString());
imposter.AddStub()
.OnPathAndMethodEqual("/api/ping", Method.Get)
.ReturnsStatus(statusCode);
this._client.Submit(imposter);
return "http://localhost:7071/api/ping";
}
}

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:

start /b mb --noLogFile
start /b func host start --csharp

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.

# Console #1
mb --noLogFile
# Console #2
func host start --csharp

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.

dotnet test [Test_Project_Name].csproj -c Release --filter:"TestCategory=Integration"

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:

If you're keen on more reading for the --filter option, this document and this document would be worth checking.

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.

More Readings