Komponententest Durable Functions und dauerhafte Aufgaben-SDKs

Komponententests für dauerhafte Orchestrierungen helfen Ihnen dabei, Geschäftslogik zu überprüfen und Fehler frühzeitig abzufangen. Orchestrierungen koordinieren mehrere Aktivitäten und können schnell komplex werden, sodass Tests vor Regressionen schützen, während sich Ihr Workflow weiterentwickelt.

Wählen Sie die Registerkarte aus, die Ihrem Projekt entspricht: Durable Functions wenn Sie Azure Functions oder Durable Task SDKs verwenden, wenn Sie das eigenständige SDK ohne Azure Functions verwenden.

Mit Durable Functions testen Sie Orchestratoren, Aktivitäten und Clientfunktionen (Trigger) durch Mocken der vom Framework bereitgestellten Kontextobjekte und indem Sie Ihre Funktionen direkt aufrufen. Durch diesen Ansatz wird Ihre Geschäftslogik von der Azure Functions Laufzeit isoliert.

Hier ist ein minimaler C#-Orchestratortest zum Anzeigen des Musters:

[Fact]
public async Task MyOrchestrator_CallsActivity()
{
    var contextMock = new Mock<TaskOrchestrationContext>();
    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.IsAny<TaskName>(), It.IsAny<string>(), It.IsAny<TaskOptions>()))
        .ReturnsAsync("result");

    var result = await MyOrchestrator.Run(contextMock.Object);

    Assert.Equal("result", result);
}

Der Rest dieses Artikels behandelt dieses Muster ausführlich für C# und Python.

Die eigenständigen sdKs für dauerhafte Aufgaben bieten integrierte Testinfrastruktur , die Orchestrierungen ohne externe Abhängigkeiten im Arbeitsspeicher ausführt. Sie registrieren Orchestratoren und Aktivitäten mit einem Test-Worker, planen Orchestrierungen über einen Test-Client und bestätigen die Ergebnisse. Für C# und JavaScript ist kein Mocking erforderlich. Python verwendet einen generatorbasierten Ansatz mit manueller Ergebniseinfügung.

Hier ist ein minimaler C#-Test zum Anzeigen des Musters:

[Fact]
public async Task MyOrchestrator_Completes()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<MyOrchestrator>();
        tasks.AddActivity<MyActivity>();
    });

    string id = await host.Client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestrator));
    var result = await host.Client.WaitForInstanceCompletionAsync(id, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus);
}

Der Rest dieses Artikels behandelt dieses Muster ausführlich für C#, Python und JavaScript.

Voraussetzungen

Testen von Orchestratorfunktionen

Orchestratorfunktionen koordinieren Aktivitäten, Zeitgeber und externe Ereignisse. Sie enthalten in der Regel die meiste Geschäftslogik und profitieren am meisten von Komponententests.

Simuliert den Orchestrierungskontext, um die Rückgabewerte von Aktivitätsaufrufen zu steuern. Rufen Sie dann Ihren Orchestrator direkt auf, und überprüfen Sie die Ausgabe.

Betrachten Sie diesen Orchestrator, der eine Aktivität dreimal aufruft:

[Function(nameof(HelloCitiesOrchestration))]
public static async Task<List<string>> HelloCities(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var outputs = new List<string>
    {
        await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"),
        await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"),
        await context.CallActivityAsync<string>(nameof(SayHello), "London")
    };

    return outputs;
}

Verwenden Sie Moq, um TaskOrchestrationContext zu mocken und erwartete Rückgabewerte für jeden Aktivitätsaufruf einzurichten.

Note

Das It.Is<TaskName>(...) Muster ist erforderlich, da CallActivityAsync eine TaskName Struktur akzeptiert und keine einfache Zeichenfolge. Moq benötigt die explizite Typ-Übereinstimmung.

[Fact]
public async Task HelloCities_ReturnsExpectedGreetings()
{
    var contextMock = new Mock<TaskOrchestrationContext>();

    // Mock each activity call to return a known value
    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(SayHello)),
        It.Is<string>(n => n == "Tokyo"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello Tokyo!");

    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(SayHello)),
        It.Is<string>(n => n == "Seattle"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello Seattle!");

    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(SayHello)),
        It.Is<string>(n => n == "London"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello London!");

    var result = await HelloCitiesOrchestration.HelloCities(contextMock.Object);

    Assert.Equal(3, result.Count);
    Assert.Equal("Hello Tokyo!", result[0]);
    Assert.Equal("Hello Seattle!", result[1]);
    Assert.Equal("Hello London!", result[2]);
}

Verwenden Sie DurableTaskTestHost, um Orchestrierungen im Arbeitsspeicher auszuführen. Registrieren Sie Ihre Produktionsorchestrator- und Aktivitätsklassen, planen Sie eine Orchestration, und bestätigen Sie das Ergebnis.

In Anbetracht dieser Produktionsklassen:

class HelloCitiesOrchestrator : TaskOrchestrator<string, List<string>>
{
    public override async Task<List<string>> RunAsync(
        TaskOrchestrationContext context, string input)
    {
        var outputs = new List<string>
        {
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "Tokyo"),
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "Seattle"),
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "London")
        };
        return outputs;
    }
}

class SayHelloActivity : TaskActivity<string, string>
{
    public override Task<string> RunAsync(TaskActivityContext context, string name)
    {
        return Task.FromResult($"Hello {name}!");
    }
}

Registrieren Sie sie direkt im Testhost:

[Fact]
public async Task HelloCities_ReturnsExpectedGreetings()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<HelloCitiesOrchestrator>();
        tasks.AddActivity<SayHelloActivity>();
    });

    string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestrator));
    OrchestrationMetadata result = await host.Client.WaitForInstanceCompletionAsync(
        instanceId, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus);

    var output = result.ReadOutputAs<List<string>>();
    Assert.Equal(3, output.Count);
    Assert.Equal("Hello Tokyo!", output[0]);
    Assert.Equal("Hello Seattle!", output[1]);
    Assert.Equal("Hello London!", output[2]);
}

DurableTaskTestHost führt ein vollständiges In-Memory-Orchestrierungsmodul aus. Es sind keine externen Dienste oder Sidecar-Prozesse erforderlich.

Testen von Aktivitätsfunktionen

Aktivitätsfunktionen enthalten die eigentliche Arbeit – aufrufen von APIs, Verarbeiten von Daten oder Interagieren mit externen Systemen. Sie sind der einfachste Funktionstyp, der getestet werden soll, da sie kein frameworkspezifisches Wiedergabeverhalten aufweisen.

Aktivitätsfunktionen in Azure Functions empfangen eine Eingabe und gegebenenfalls ein FunctionContext. Testen Sie sie wie jede andere Funktion:

[Function(nameof(SayHello))]
public static string SayHello(
    [ActivityTrigger] string name, FunctionContext executionContext)
{
    return $"Hello {name}!";
}
[Fact]
public void SayHello_ReturnsExpectedGreeting()
{
    var result = HelloCitiesOrchestration.SayHello("Tokyo", Mock.Of<FunctionContext>());
    Assert.Equal("Hello Tokyo!", result);
}

Aktivitätsfunktionen empfangen ein Kontextobjekt und eine Eingabe. Der Kontext stellt Metadaten wie die Orchestrierungs-ID und die Aufgaben-ID bereit, aber die meisten Tests benötigen sie nicht.

Rufen Sie mithilfe der SayHelloActivity Klasse aus dem Orchestratorbeispiel RunAsync direkt mit einem simulierten Kontext auf:

[Fact]
public async Task SayHello_ReturnsExpectedGreeting()
{
    var activity = new SayHelloActivity();
    var contextMock = new Mock<TaskActivityContext>();

    var result = await activity.RunAsync(contextMock.Object, "Tokyo");

    Assert.Equal("Hello Tokyo!", result);
}

Wenn Sie DurableTaskTestHost verwenden, werden Aktivitäten auch als Teil des Orchestrierungstests ausgeführt. Sie benötigen keine separaten Aktivitätstests, es sei denn, die Aktivität verfügt über komplexe Logik.

Testen von Clientfunktionen

Clientfunktionen (auch als Triggerfunktionen bezeichnet) starten Orchestrierungen und verwalten Instanzen. Sie verwenden die dauerhafte Clientbindung, um mit dem Orchestrierungsmodul zu interagieren.

Erwägen Sie diesen HTTP-Trigger, der eine Orchestrierung startet:

[Function("HelloCitiesOrchestration_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    [DurableClient] DurableTaskClient client,
    FunctionContext executionContext)
{
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestration));
    return await client.CreateCheckStatusResponseAsync(req, instanceId);
}

Simuliert DurableTaskClient , um eine bekannte Instanz-ID zurückzugeben:

[Fact]
public async Task HttpStart_ReturnsAccepted()
{
    var durableClientMock = new Mock<DurableTaskClient>("testClient");
    var functionContextMock = new Mock<FunctionContext>();
    var instanceId = "test-instance-id";

    durableClientMock
        .Setup(x => x.ScheduleNewOrchestrationInstanceAsync(
            It.IsAny<TaskName>(),
            It.IsAny<object>(),
            It.IsAny<StartOrchestrationOptions>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(instanceId);

    var mockRequest = CreateMockHttpRequest(functionContextMock.Object);

    var responseMock = new Mock<HttpResponseData>(functionContextMock.Object);
    responseMock.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.Accepted);

    durableClientMock
        .Setup(x => x.CreateCheckStatusResponseAsync(
            It.IsAny<HttpRequestData>(),
            It.IsAny<string>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(responseMock.Object);

    var result = await HelloCitiesOrchestration.HttpStart(
        mockRequest, durableClientMock.Object, functionContextMock.Object);

    Assert.Equal(HttpStatusCode.Accepted, result.StatusCode);
}

Testen von Clientvorgängen

Mit den eigenständigen SDKs für dauerhafte Aufgaben nutzen Clientvorgänge (Orchestrierungen planen, Status abfragen, Ereignisse auslösen) dasselbe TestOrchestrationClient, wie es bereits in den Orchestratortests dargestellt wurde. Es ist keine separate Clientfunktion vorhanden – Sie rufen die Client-API direkt auf.

DurableTaskTestHost enthält host.Client, das eine voll funktionsfähige DurableTaskClient ist. Verwenden Sie diese, um Vorgänge auf Client-Ebene wie das Planen, Abfragen oder Beenden von Orchestrationen zu testen.

[Fact]
public async Task Client_CanQueryOrchestrationStatus()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<HelloCitiesOrchestrator>();
        tasks.AddActivity<SayHelloActivity>();
    });

    string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestrator));

    // Query status while the orchestration runs
    OrchestrationMetadata metadata = await host.Client.WaitForInstanceCompletionAsync(
        instanceId, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus);
    Assert.Equal(instanceId, metadata.InstanceId);
}