Unit test per Durable Functions e Durable Task SDK

L'esecuzione di unit test delle orchestrazioni durevoli consente di verificare la logica di business e rilevare gli errori in anticipo. Le orchestrazioni coordinano più attività e possono aumentare rapidamente le attività complesse, quindi i test proteggono dalle regressioni man mano che il flusso di lavoro si evolve.

Selezionare la scheda corrispondente al progetto: Durable Functions se si usa Funzioni di Azure o Durable Task SDK se si usa l'SDK autonomo senza Funzioni di Azure.

Con Durable Functions, è possibile testare orchestratori, attività e funzioni client (trigger) simulando gli oggetti di contesto forniti dal framework e chiamare direttamente le funzioni. Questo approccio isola la logica di business dal runtime di Funzioni di Azure.

Ecco un test minimo dell'agente di orchestrazione C# per mostrare il modello:

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

Il resto di questo articolo illustra in dettaglio questo modello per C# e Python.

Gli SDK di Durable Task autonomi forniscono un'infrastruttura di test predefinita che esegue orchestrazioni in memoria senza dipendenze esterne. Registrare agenti di orchestrazione e attività con un processo di lavoro di test, pianificare orchestrazioni tramite un client di test e verificare i risultati. Non è necessaria alcuna simulazione per C# e JavaScript. Python usa un approccio basato su generatore con l'inserimento manuale dei risultati.

Ecco un test C# minimo per mostrare il modello:

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

Il resto di questo articolo illustra in dettaglio questo modello per C#, Python e JavaScript.

Prerequisiti

  • xUnit : framework di test
  • Moq: framework di comportamento fittizio
  • Familiarità con il modello .NET di lavoro isolato

Testare le funzioni dell'orchestratore

Le funzioni dell'agente di orchestrazione coordinano le attività, i timer e gli eventi esterni. In genere contengono la logica di business più vantaggiosa e traggono il massimo vantaggio dagli unit test.

Simulare il contesto di orchestrazione per controllare i valori restituiti delle chiamate di attività. Chiamare quindi direttamente l'agente di orchestrazione e verificare l'output.

Considerare questo orchestratore che chiama un'attività tre volte:

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

Usare Moq per simulare TaskOrchestrationContext e configurare i valori restituiti previsti per ogni chiamata di attività.

Note

Il It.Is<TaskName>(...) criterio è obbligatorio perché CallActivityAsync accetta uno TaskName struct, non una stringa normale. Moq richiede la corrispondenza esplicita del tipo.

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

Usare DurableTaskTestHost per eseguire orchestrazioni in memoria. Registrare l'agente di orchestrazione di produzione e le classi di attività, pianificare un'orchestrazione e verificare il risultato.

Date queste classi di produzione:

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

Registrarli direttamente nell'host di 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 esegue un motore di orchestrazione in memoria completo. Non sono necessari servizi esterni o processi sidecar.

Funzioni di attività di test

Le funzioni di attività contengono il lavoro effettivo, ovvero la chiamata di API, l'elaborazione dei dati o l'interazione con sistemi esterni. Sono il tipo di funzione più semplice da testare perché non hanno un comportamento di riproduzione specifico del framework.

Le funzioni di attività in Funzioni di Azure ricevono un input e facoltativamente un elemento FunctionContext. Testarli come qualsiasi altra funzione:

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

Le funzioni di attività ricevono un oggetto contesto e un input. Il contesto fornisce metadati come l'ID di orchestrazione e l'ID attività, ma la maggior parte dei test non ne ha bisogno.

Usando la SayHelloActivity classe dell'esempio dell'agente di orchestrazione, chiamare RunAsync direttamente con un contesto fittizio:

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

Quando si usa DurableTaskTestHost, le attività vengono eseguite anche come parte del test di orchestrazione. Non sono necessari test di attività separati, a meno che l'attività non abbia una logica complessa.

Testare le funzioni client

Le funzioni client ,dette anche funzioni trigger, avviano orchestrazioni e gestiscono le istanze. Usano l'associazione client durevole per interagire con il motore di orchestrazione.

Si consideri questo trigger HTTP che avvia un'orchestrazione:

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

Simulare DurableTaskClient per restituire un ID istanza noto:

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

Testare le operazioni client

Con gli SDK di Durable Task autonomi, le operazioni client (pianificazione di orchestrazioni, query di stato, generazione di eventi) usano gli stessi TestOrchestrationClient già visualizzati nei test dell'agente di orchestrazione. Non esiste alcuna funzione client separata: è possibile chiamare direttamente l'API client.

DurableTaskTestHostespone host.Client, che è un DurableTaskClient completamente funzionale. Utilizzalo per testare operazioni lato client, come la pianificazione, l'esecuzione di query o il terminare delle orchestrazioni.

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