AzureFunctions.TestHelpers
4.0.109
dotnet add package AzureFunctions.TestHelpers --version 4.0.109
NuGet\Install-Package AzureFunctions.TestHelpers -Version 4.0.109
<PackageReference Include="AzureFunctions.TestHelpers" Version="4.0.109" />
paket add AzureFunctions.TestHelpers --version 4.0.109
#r "nuget: AzureFunctions.TestHelpers, 4.0.109"
// Install AzureFunctions.TestHelpers as a Cake Addin #addin nuget:?package=AzureFunctions.TestHelpers&version=4.0.109 // Install AzureFunctions.TestHelpers as a Cake Tool #tool nuget:?package=AzureFunctions.TestHelpers&version=4.0.109
AzureFunctions.TestHelpers ⚡
Test your Azure Functions! Spin up integration tests. By combining bits and pieces of the WebJobs SDK, Azure Functions and Durable Functions and adding some convenience classes and extension methods on top.
You'll ❤ the feedback!
Updates
- v4.0: Update to Azure Functions SDK v4
- v3.3: Allow to pass a retry delay on Wait and Ready methods
- v3.2: Updated dependencies, Ready also ignored durable entities
- v3.1: WaitFor to better support durable entities
- v3.0: Upgrade to durable task v2
- v2.1: Removed AddDurableTaskInTestHub
- v2.0: Wait, ThrowIfFailed and Purge separated.
Use Startup class
If only inheriting from IWebJobsStartup
new HostBuilder()
.ConfigureWebJobs(builder => builder
.UseWebJobsStartup<Startup>())
.Build();
If inheriting from FunctionsStartup
new HostBuilder()
.ConfigureWebJobs(builder => builder
.UseWebJobsStartup(typeof(Startup), new WebJobsBuilderContext(), NullLoggerFactory.Instance))
.Build();
Configure Services for Dependency Injection
I just found out the default ConfigureServices
on the HostBuilder
also works.
But if it makes more sense to you to configure services on the WebJobsBuilder
since
you also configure the Startup
there you can use:
mock = Substitute.For<IInjectable>();
host = new HostBuilder()
.ConfigureWebJobs(builder => builder
.UseWebJobsStartup<Startup>()
.ConfigureServices(services => services.Replace(ServiceDescriptor.Singleton(mock))))
.Build();
Register and replace services that are injected into your functions.
Include Microsoft.Azure.Functions.Extensions
in your test project to enable dependency injection!
Note: Not sure if this is still a requirement for Azure Functions >= v2.0
.
HTTP Triggered Functions
Invoke a regular http triggered function:
[Fact]
public static async Task HttpTriggeredFunctionWithDependencyReplacement()
{
// Arrange
var mock = Substitute.For<IInjectable>();
using (var host = new HostBuilder()
.ConfigureWebJobs(builder => builder
.AddHttp()
.ConfigureServices(services => services.AddSingleton(mock)))
.Build())
{
await host.StartAsync();
var jobs = host.Services.GetService<IJobHost>();
// Act
await jobs.CallAsync(nameof(DemoHttpFunction), new Dictionary<string, object>
{
["request"] = new DummyHttpRequest()
});
// Assert
mock
.Received()
.Execute();
}
}
HTTP Request
Because you can't invoke an HTTP-triggered function without a request, and I couldn't find one
in the standard libraries, I created the DummyHttpRequest
.
await jobs.CallAsync(nameof(DemoInjection), new Dictionary<string, object>
{
["request"] = new DummyHttpRequest("{ \"some-key\": \"some value\" }")
});
New: Now you can set string content via the constructor overload!
You can set all kinds of regular settings on the request when needed:
var request = new DummyHttpRequest
{
Scheme = "http",
Host = new HostString("some-other"),
Headers = {
["Authorization"] = $"Bearer {token}",
["Content-Type"] = "application/json"
}
};
New: Now you can use a DummyQueryCollection to mock the url query:
var request = new DummyHttpRequest
{
Query = new DummyQueryCollection
{
["firstname"] = "Jane",
["lastname"] = "Doe"
}
};
HTTP Response
To capture the result(s) of http-triggered functions you use the options.SetResponse
callback on the AddHttp
extension method:
// Arrange
var observer = Observer.For<object>();
using (var host = new HostBuilder()
.ConfigureWebJobs(builder => builder
.AddHttp(options => options.SetResponse = (_, o) => observer.Add(o)))
.Build())
{
await host.StartAsync();
var jobs = host.Services.GetService<IJobHost>();
// Act
await jobs.CallAsync(nameof(DemoHttpFunction), new Dictionary<string, object>
{
["request"] = new DummyHttpRequest()
});
}
// Assert
await Hypothesis
.On(observer)
.Timebox(2.Seconds())
.Any()
.Match(o => o is OkResult)
.Validate();
I'm using Hypothesist for easy async testing.
Durable Functions
Invoke a (time-triggered) durable function:
[Fact]
public static async Task DurableFunction()
{
// Arrange
var mock = Substitute.For<IInjectable>();
using (var host = new HostBuilder()
.ConfigureWebJobs(builder => builder
.AddDurableTask(options => options.HubName = nameof(MyTestFunction))
.AddAzureStorageCoreServices()
.ConfigureServices(services => services.AddSingleton(mock)))
.Build())
{
await host.StartAsync();
var jobs = host.Services.GetService<IJobHost>();
await jobs.
Terminate()
.Purge();
// Act
await jobs.CallAsync(nameof(DemoStarter), new Dictionary<string, object>
{
["timerInfo"] = new TimerInfo(new WeeklySchedule(), new ScheduleStatus())
});
await jobs
.Ready()
.ThrowIfFailed()
.Purge();
// Assert
mock
.Received()
.Execute();
}
}
You'll have to configure Azure WebJobs Storage to run durable functions!
Time Triggered Functions
Do NOT add timers to the web jobs host!
using (var host = new HostBuilder()
.ConfigureWebJobs(builder => builder
//.AddTimers() <-- DON'T ADD TIMERS
.AddDurableTask(options => options.HubName = nameof(MyTestFunction))
.AddAzureStorageCoreServices()
.ConfigureServices(services => services.AddSingleton(mock)))
.Build())
{
}
}
It turns out it is not required to invoke time-triggered functions, and by doing so your functions will be triggered randomly, messing up the status of your orchestration instances.
Isolate Durable Functions
Add and configure Durable Functions using the durable task extensions and use a specific hub name to isolate from other parallel tests.
host = new HostBuilder()
.ConfigureWebJobs(builder => builder
.AddDurableTask(options => options.HubName = nameof(MyTestFunction))
.AddAzureStorageCoreServices()
.Build();
BREAKING: In v2.1
I removed the AddDurableTaskInTestHub()
method. You can easily do it yourself with
AddDurableTask(options => ...)
and be more specific about the context of your test. This way, you don't
end up with hundreds of empty history and instance tables in your storage account.
Cleanup
await jobs
.Terminate()
.Purge();
To cleanup from previous runs, you terminate leftover orchestrations and durable entities and purge the history.
WaitFor
await jobs
.WaitFor(nameof(DemoOrchestration), TimeSpan.FromSeconds(30))
.ThrowIfFailed();
With the WaitFor
you specify what orchestration you want to wait for.
You can either use the Ready
function if you just want all orchestrations to complete.
Ready
await jobs
.Ready(TimeSpan.FromSeconds(30))
.ThrowIfFailed();
The Ready
function is handy if you want to wait for termination.
BREAKING: In v2
the WaitForOrchestrationsCompletion
is broken down into Wait()
, ThrowIfFailed()
and Purge()
.
Reuse
When injecting a configured host into your test, make sure you do NOT initialize nor clean it
in the constructor. For example, when using xUnit
you use the IAsyncLifetime
for that, otherwise your test will probably hang forever.
Initialize and start the host in a fixture:
public class HostFixture : IDisposable, IAsyncLifetime
{
private readonly IHost _host;
public IJobHost Jobs => _host.Services.GetService<IJobHost>();
public HostFixture() =>
_host = new HostBuilder()
.ConfigureWebJobs(builder => builder
.AddDurableTask(options => options.HubName = nameof(MyTest))
.AddAzureStorageCoreServices())
.Build();
public void Dispose() =>
_host.Dispose();
public Task InitializeAsync() =>
_host.StartAsync();
public Task DisposeAsync() =>
Task.CompletedTask;
}
Inject and cleanup the host in the test class:
public class MyTest : IClassFixture<HostFixture>, IAsyncLifetime
{
private readonly HostFixture _host;
public MyTest(HostFixture host) =>
_host = host;
public Task InitializeAsync() =>
_host.Jobs
.Terminate()
.Purge();
public Task DisposeAsync() =>
Task.CompletedTask;
}
But please, don't to do a ConfigureAwait(false).GetAwaiter().GetResult()
.
Using ConfigureAwait(false) to avoid deadlocks is a dangerous practice. You would have to use ConfigureAwait(false) for every await in the transitive closure of all methods called by the blocking code, including all third- and second-party code. Using ConfigureAwait(false) to avoid deadlock is at best just a hack).
Queue Triggered Functions
// Arrange
using (var host = new HostBuilder()
.ConfigureWebJobs(builder => builder
.AddAzureStorageQueues()
.ConfigureServices(services => services.AddSingleton(mock)))
.Build())
{
await host.StartAsync();
var jobs = host.Services.GetService<IJobHost>();
// Act
await jobs.CallAsync(nameof(DemoQueueFunction), new Dictionary<string, object>
{
["queueItem"] = ""
});
}
Azure Storage Account
You need an azure storage table to store the state of the durable functions. The two options currently are Azure and the Azurite.
Option 1: Azure
Just copy the connection string from your storage account, works everywhere.
Option 2: Azurite
azurite@v3
does have the required features implemented now!
See test and fixture for using docker to host azurite in a container, or checkout the docs on how to run it on your system..
Set the Storage Connection String
The storage connection string setting is required.
Option 1: with an environment variable
Set the environment variable AzureWebJobsStorage
. Hereby you can also overwrite the configured connection from option 2 on your local dev machine.
Option 2: with a configuration file
Include an appsettings.json
in your test project:
{
"AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...==;EndpointSuffix=core.windows.net"
}
and make sure it is copied to the output directory:
<ItemGroup>
<Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Happy coding!
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.1 is compatible. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Microsoft.Azure.WebJobs.Extensions.DurableTask (>= 2.6.0 && < 3.0.0)
- Microsoft.NET.Sdk.Functions (>= 4.0.0 && < 5.0.0)
-
net6.0
- Microsoft.Azure.WebJobs.Extensions.DurableTask (>= 2.6.0 && < 3.0.0)
- Microsoft.NET.Sdk.Functions (>= 4.0.0 && < 5.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
4.0.109 | 13,331 | 3/1/2024 |
4.0.108 | 154 | 3/1/2024 |
4.0.107 | 122 | 3/1/2024 |
4.0.106 | 128 | 3/1/2024 |
4.0.105 | 120 | 3/1/2024 |
4.0.101 | 19,411 | 11/28/2022 |
4.0.98 | 372 | 11/28/2022 |
3.3.87 | 63,411 | 11/17/2021 |
3.3.85 | 129,427 | 6/14/2021 |
3.2.83 | 7,409 | 4/24/2020 |
3.2.81 | 556 | 4/23/2020 |
3.2.80 | 559 | 4/23/2020 |
3.2.76 | 592 | 4/23/2020 |
3.1.74 | 1,115 | 11/19/2019 |
3.0.71 | 643 | 11/12/2019 |
3.0.69 | 625 | 11/12/2019 |
3.0.68 | 611 | 11/12/2019 |
2.1.67 | 618 | 11/6/2019 |
2.1.64 | 594 | 11/6/2019 |
2.1.63 | 619 | 11/6/2019 |
2.1.62 | 615 | 11/5/2019 |
2.0.59 | 615 | 11/5/2019 |
2.0.58 | 621 | 11/5/2019 |
2.0.57 | 632 | 11/5/2019 |
2.0.53 | 647 | 11/5/2019 |
1.0.39 | 726 | 10/7/2019 |
1.0.38 | 672 | 10/3/2019 |
1.0.37 | 645 | 10/3/2019 |
1.0.36 | 599 | 10/3/2019 |
1.0.35 | 641 | 10/1/2019 |
0.0.33 | 754 | 9/20/2019 |
0.0.32 | 705 | 9/20/2019 |
0.0.30 | 654 | 9/5/2019 |
0.0.29 | 634 | 9/2/2019 |
0.0.26 | 672 | 8/27/2019 |
0.0.22 | 708 | 8/1/2019 |
0.0.21 | 677 | 7/29/2019 |
0.0.20 | 654 | 7/26/2019 |
0.0.18 | 655 | 7/26/2019 |
0.0.17 | 665 | 7/26/2019 |
0.0.16 | 682 | 7/26/2019 |
0.0.14 | 636 | 7/26/2019 |
0.0.13 | 703 | 7/26/2019 |
0.0.3 | 639 | 7/9/2019 |
0.0.2 | 664 | 7/9/2019 |
Update to functions sdk v4