Testing Azure Functions on Azure DevOps – part 1: setup
Testing Azure Functions on Azure DevOps – part 1: setup

Testing Azure Functions on Azure DevOps – part 1: setup

2020, Sep 07    

Hi All! Today we’re going to talk a bit about testing strategies for Azure Functions. We’ll see how setup our test framework and in another article, we’ll see how to create a build pipeline on Azure DevOps.

As part of my daily job, I’m spending a lot of time working with Azure and Azure Functions. These days I’m also working a lot with Durable Entities, which open the door to even more scenarios. Anyways, no matter what’s the technology behind, one of the best ways to ensure that our software is reliable is to add automatic tests. And these tests have to be part of the build pipeline.

Now, based on my researches so far, we can’t create a Functions Host directly as we could do for a “regular” WebAPI. What we can do instead is make use of the Azure Function Core Tools and manually (aka via code) spin up the host in an XUnit Fixture.

This has the only drawback that when running the tests locally we won’t be able to debug the Function code. However, keep in mind the goal here: we want to test the boundaries of our services by probing the various Function Triggers.

And this is a form of Black Box Testing: we’re not supposed to know what’s inside the box, only how to operate it.

If we need to debug, we can always run the Function project directly from VS and check the behaviour via Postman (if it’s a REST endpoint). Just sayin’.

Moreover, as stated before, we will be executing those tests in our build pipeline, so debugging is not our primary interest.

Anyways, let’s just into the code! The first thing to do, assuming we already have an Azure Functions project, is to create the Test project, add a reference to XUnit and create a Fixture:

public class AzureFunctionsFixture : IDisposable
{
	private readonly Process _funcHostProcess;	

	public readonly HttpClient Client;

	public AzureFunctionsFixture()
	{		
		var port = /*get this from config*/
		var dotnetExePath = /*get this from config*/
		var functionHostPath = /*get this from config*/		
		var functionAppFolder = /*get this from config*/

		_funcHostProcess = new Process
		{
			StartInfo =
			{
				FileName = dotnetExePath,
				Arguments = $"\"{functionHostPath}\" start -p {port}",
				WorkingDirectory = functionAppFolder
			}
		};
		var success = _funcHostProcess.Start();
		if (!success || _funcHostProcess.HasExited)
			throw new InvalidOperationException("Could not start Azure Functions host.");

		this.Client = new HttpClient();
		this.Client.BaseAddress = new Uri($"http://localhost:{port}");
	}
}

As you can see, in the cTor we’re reading few values from the configuration:

  • the path to dotnet.exe
  • the path to func.dll from the Azure Functions Core Tools
  • the path to our Azure Functions DLL
  • the port we want to use to expose the host

The Process class will be basically running something like:

dotnet "%APPDATA%\npm\node_modules\azure-functions-core-tools\bin\func.dll" start -p 7071 

from the bin\Debug (or Release) directory of our Azure Functions project.

We’re also creating and publicly exposing an HttpClient: our tests will be using it to “talk” with the Functions Host. To keep things simple, I’m assuming that we’re using only HTTP Triggers.

As some of you might have noticed, the Fixture class is also implementing IDisposable, to properly dispose of the Process and of the HttpClient:

public void Dispose()
{
	this.Client?.Dispose();

	if (null != _funcHostProcess)
	{
		if (!_funcHostProcess.HasExited)
			_funcHostProcess.Kill();

		_funcHostProcess.Dispose();
	}
}

The next thing to do is to create our test class as usual. Now, it’s quite likely that we might want to split our tests into multiple classes.

In this case, we have to make sure to not spin up more than one Functions Host.

Luckily, XUnit comes to the rescue with Collection Fixtures. All we have to do is to create an empty class and mark it with the CollectionDefinition attribute:

[CollectionDefinition(nameof(AzureFunctionsTestsCollection ))]
public class AzureFunctionsTestsCollection : ICollectionFixture { }

Now we can decorate our test classes with all the necessary attributes and inject the Fixture:

[Collection(nameof(AzureFunctionsTestsCollection))]
[Category("Contract")]
[Trait("Category", "Contract")]
public class TriggerWorkflowTests
{
	private readonly AzureFunctionsFixture _fixture;

	public TriggerWorkflowTests(AzureFunctionsFixture fixture)
	{
		_fixture = fixture;
	}

	[Fact]
	public async Task FooFunc_should_do_something_and_not_fail_miserably()
	{
		var response = await _fixture.Client.GetAsync("api/foo");
		response.IsSuccessStatusCode.Should().BeTrue();
	}
}

That’s all for today! Next time we’ll push our Azure Functions to the repository and make sure the build pipeline runs fine. Ciao!

Did you like this post? Then