Integration testing Azure Functions can be approached somewhat similar to how one may write integration tests for WebAPIs. The key difference is that while an HTTP test server will actually trigger your resources in the API, you will need to trigger the method in your function to be tested by hand.
The complete source code can be found on GitHub.
Notable NuGet packages
In order to be able to make use of DI for Azure Functions, you need to add Microsoft.Azure.Functions.Extensions
Startup.cs
[assembly: FunctionsStartup(typeof(Startup))]
namespace FunctionApp1;
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IMyService, MyService>();
BootstrapMyIckyDependency(builder);
}
public virtual void BootstrapMyIckyDependency(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<MyIckyDependency>();
}
}
Using the virtual method approach is my preferred way of overriding the different moving parts for the system under test in a TestStartup class.
The function we’d like to test
Nothing much to take not of here. For this example I have used a HTTP triggered function. As we will see later, it really does not matter, because we will need to trigger the Run() method ourselves.
public class Function1
{
private readonly IMyService _myService;
private readonly IMyIckyDependency _myIckyDependency;
public Function1(IMyService myService, IMyIckyDependency myIckyDependency)
{
_myService = myService;
_myIckyDependency = myIckyDependency;
}
[FunctionName("Function1")]
public Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
{
var resultFromMyService = _myService.MyMethod("Hello");
var resultFromMyOtherService = _myIckyDependency.Hello();
return Task.FromResult<IActionResult>(new OkObjectResult($"{resultFromMyService} {resultFromMyOtherService}"));
}
}
TestStartup.cs
public class TestStartup : Startup
{
public override void Configure(IFunctionsHostBuilder builder)
{
base.Configure(builder);
builder.Services.AddTransient<Function1>(); //We need to register the function by hand when running the test
builder.Services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true });
}
public override void BootstrapMyIckyDependency(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton(A.Fake<IMyIckyDependency>());
}
}
Overriding the virtual method(s) from the Startup class lets you shave off any dependencies that might be problematic to interact with in an integration test scenario. Functions triggered by Azure Service Bus messages for starters – are something that probably should depend on a mock when running integration tests in many scenarios.
Writing a test
As mentioned earlier – in order to trigger the function itself, we will need to call the Run() method ourselves. The method however, is called on the actual function running in the function host. By doing so, the whole application is bootstrapped like it would have when running in Azure.
Any icky services that for some reason might not be desired to wire up, can easily be faked when in the TestStartup class – and their responses may be fabricated in the different tests.
public class Function1Test
{
[Fact]
public async Task Run_WhenCalled_ShouldDoStuff()
{
//arrange
var hostBuilder = new HostBuilder();
hostBuilder.ConfigureWebJobs(b => b.UseWebJobsStartup(typeof(TestStartup), new WebJobsBuilderContext(), NullLoggerFactory.Instance));
var host = hostBuilder.Start();
A.CallTo(() => host.Services.GetRequiredService<IMyIckyDependency>().Hello()).Returns("World!");
//act
var result = await host.Services.GetRequiredService<Function1>().Run(null).ConfigureAwait(continueOnCapturedContext: false);
//assert
Assert.Equal("Hello World!", ((OkObjectResult)result).Value);
}
}