Test unitaire de Durable Functions et des SDK Durable Task

Les tests unitaires d’orchestrations durables vous aident à vérifier la logique métier et à détecter les erreurs au début. Les orchestrations coordonnent plusieurs activités et peuvent croître rapidement, de sorte que les tests protègent contre les régressions à mesure que votre flux de travail évolue.

Sélectionnez l’onglet correspondant à votre projet : Durable Functions si vous utilisez Azure Functions ou Durable Task SDK si vous utilisez le SDK autonome sans Azure Functions.

Avec Durable Functions, vous testez les fonctions d'orchestration, d'activités et de client (déclencheur) en simulant les objets de contexte fournis par le cadre et en appelant directement vos fonctions. Cette approche isole votre logique métier du runtime Azure Functions.

Voici un test d’orchestrateur C# minimal pour afficher le modèle :

[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);
}

Le reste de cet article décrit ce modèle en détail pour C# et Python.

Les kits SDK Durable Task autonomes fournissent une infrastructure de test intégrée qui exécute des orchestrations en mémoire sans dépendances externes. Vous inscrivez les orchestrateurs et les activités auprès d’un worker de test, planifiez des orchestrations via un client de test et validez les résultats. Aucune technique de mocking n’est requise pour C# et JavaScript. Python utilise une approche basée sur un générateur avec l’injection manuelle de résultats.

Voici un test C# minimal pour afficher le modèle :

[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);
}

Le reste de cet article décrit ce modèle en détail pour C#, Python et JavaScript.

Prerequisites

Tester les fonctions d’orchestrateur

Les fonctions Orchestrator coordonnent les activités, les minuteurs et les événements externes. Ils contiennent généralement la logique métier la plus grande et bénéficient le plus des tests unitaires.

Simulez le contexte d’orchestration afin de contrôler les valeurs de retour des appels d’activité. Appelez ensuite votre orchestrateur directement et vérifiez la sortie.

Considérez cet orchestrateur qui appelle une activité trois fois :

[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;
}

Utilisez Moq pour simuler TaskOrchestrationContext et configurer les valeurs de retour attendues pour chaque appel d’activité.

Note

Le It.Is<TaskName>(...) modèle est requis, car CallActivityAsync accepte un TaskName struct, et non une chaîne simple. Moq nécessite une correspondance explicite de types.

[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]);
}

Utilisez DurableTaskTestHost pour exécuter des orchestrations en mémoire. Inscrivez vos classes d’orchestrateurs et d’activités de production, planifiez une orchestration, puis validez le résultat.

Compte tenu de ces classes de production :

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}!");
    }
}

Inscrivez-les directement dans l’hôte de test :

[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 exécute un moteur d’orchestration en mémoire complet. Aucun service externe ni processus sidecar n’est requis.

Fonctions d’activité de test

Les fonctions d’activité contiennent le travail réel : appel d’API, traitement des données ou interaction avec des systèmes externes. Il s’agit du type de fonction le plus simple à tester, car il n’a pas de comportement de reprise spécifique au framework.

Les fonctions d’activité dans Azure Functions reçoivent une entrée et éventuellement une FunctionContext. Testez-les comme n’importe quelle autre fonction :

[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);
}

Les fonctions d’activité reçoivent un objet de contexte et une entrée. Le contexte fournit des métadonnées telles que l’ID d’orchestration et l’ID de tâche, mais la plupart des tests n’en ont pas besoin.

À l’aide de la SayHelloActivity classe de l’exemple d’orchestrateur, appelez RunAsync directement avec un contexte fictif :

[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);
}

Lorsque vous utilisez DurableTaskTestHost, les activités s’exécutent également dans le cadre du test d’orchestration. Vous n’avez pas besoin de tests d’activité distincts, sauf si l’activité a une logique complexe.

Tester les fonctions clientes

Les fonctions clientes (également appelées fonctions de déclencheur) démarrent des orchestrations et gèrent des instances. Ils utilisent la liaison du client durable pour interagir avec le moteur d’orchestration.

Tenez compte de ce déclencheur HTTP qui démarre une orchestration :

[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);
}

Simuler DurableTaskClient pour retourner un ID d’instance connu :

[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);
}

Tester les opérations du client

Avec les SDK Durable Task autonomes, les opérations côté client (orchestrations d'ordonnancement, consultation de l'état, envoi d'événements) utilisent les mêmes TestOrchestrationClient déjà montrés dans les tests d’orchestrateur. Aucune fonction cliente distincte n’existe : vous appelez directement l’API cliente.

DurableTaskTestHost host.Client expose, qui est un DurableTaskClient entièrement fonctionnel. Utilisez-le pour tester les opérations du client, telles que la planification, la requête ou l'arrêt des orchestrations.

[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);
}