Freigeben über


Komponententests für Durable Functions (C# in-process)

Komponententests sind ein wichtiger Bestandteil moderner Softwareentwicklungsmethoden. Mit Komponententests wird das Verhalten der Geschäftslogik überprüft und verhindert, dass in der Zukunft unbemerkte Breaking Changes eingeführt werden. Dauerhafte Funktionen können leicht komplexer werden, sodass die Einführung von Komponententests dazu beiträgt, unterbrechungsfähige Änderungen zu vermeiden. In den folgenden Abschnitten wird erläutert, wie die drei Funktionstypen - Orchestration Client, Orchestrator und Aktivitätsfunktionen - einem Unit-Test unterzogen werden können.

Hinweis

Dieser Artikel bietet Leitlinien für Unit Tests von Apps mit Durable Functions, die in C# für den .NET-In-Process-Worker geschrieben wurden und sich an Durable Functions 2.x richten. Weitere Informationen zu den Unterschieden zwischen den Versionen finden Sie im Artikel Durable Functions-Versionen.

Voraussetzungen

Die Beispiele in diesem Artikel erfordern Kenntnisse der folgenden Konzepte und Frameworks:

  • Komponententests

  • Dauerhafte Funktionen

  • xUnit – Test-Framework

  • moq – Simulationsframework

Basisklassen für Simulation

Mocking wird über die folgende Schnittstelle unterstützt:

Diese Schnittstellen können mit den verschiedenen Triggern und Bindungen verwendet werden, die von Durable Functions unterstützt werden. Während sie Ihre Azure Functions ausführt, führt die Funktionslaufzeit Ihren Funktionscode mit einer konkreten Implementierung dieser Schnittstellen aus. Für Komponententests können Sie eine simulierte Version dieser Schnittstellen übergeben, um Ihre Geschäftslogik zu testen.

Triggerfunktionen für Komponententests

In diesem Abschnitt überprüft der Komponententest die Logik der folgenden HTTP-Triggerfunktion zum Starten neuer Orchestrierungen.

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace VSSample
{
    public static class HttpStart
    {
        [FunctionName("HttpStart")]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
            [DurableClient] IDurableClient starter,
            string functionName,
            ILogger log)
        {
            // Function input comes from the request content.
            object eventData = await req.Content.ReadAsAsync<object>();
            string instanceId = await starter.StartNewAsync(functionName, eventData);

            log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

            return starter.CreateCheckStatusResponse(req, instanceId);
        }
    }
}

Die Komponententestaufgabe überprüft den Wert des in der Antwortnutzlast bereitgestellten Retry-After-Headers. Daher simuliert der Komponententest einige Methoden IDurableClient , um ein vorhersagbares Verhalten sicherzustellen.

Zunächst verwenden wir ein Mocking-Framework (moq in diesem Fall), um IDurableClient zu simulieren.

// Mock IDurableClient
var durableClientMock = new Mock<IDurableClient>();

Hinweis

Während Sie Schnittstellen modellieren können, indem Sie die Schnittstelle direkt als Klasse implementieren, vereinfachen Mocking-Frameworks den Prozess auf verschiedene Weise. Wenn beispielsweise eine neue Methode über Nebenversionen hinweg zur Schnittstelle hinzugefügt wird, erfordert moq keine Codeänderungen im Gegensatz zu konkreten Implementierungen.

Dann wird die Methode StartNewAsync simuliert, um eine bekannte Instanz-ID zurückzugeben.

// Mock StartNewAsync method
durableClientMock.
    Setup(x => x.StartNewAsync(functionName, It.IsAny<object>())).
    ReturnsAsync(instanceId);

Als Nächstes wird CreateCheckStatusResponse so simuliert, dass es immer eine leere HTTP 200-Antwort zurückgibt.

// Mock CreateCheckStatusResponse method
durableClientMock
    // Notice that even though the HttpStart function does not call IDurableClient.CreateCheckStatusResponse() 
    // with the optional parameter returnInternalServerErrorOnFailure, moq requires the method to be set up
    // with each of the optional parameters provided. Simply use It.IsAny<> for each optional parameter
    .Setup(x => x.CreateCheckStatusResponse(It.IsAny<HttpRequestMessage>(), instanceId, returnInternalServerErrorOnFailure: It.IsAny<bool>()))
    .Returns(new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = new StringContent(string.Empty),
        Headers =
        {
            RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10))
        }
    });

ILogger wird ebenfalls simuliert:

// Mock ILogger
var loggerMock = new Mock<ILogger>();

Nun wird die Run Methode aus dem Komponententest aufgerufen:

// Call Orchestration trigger function
var result = await HttpStart.Run(
    new HttpRequestMessage()
    {
        Content = new StringContent("{}", Encoding.UTF8, "application/json"),
        RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
    },
    durableClientMock.Object,
    functionName,
    loggerMock.Object);

Der letzte Schritt besteht darin, die Ausgabe mit dem erwarteten Wert zu vergleichen:

// Validate that output is not null
Assert.NotNull(result.Headers.RetryAfter);

// Validate output's Retry-After header value
Assert.Equal(TimeSpan.FromSeconds(10), result.Headers.RetryAfter.Delta);

Nachdem Sie alle diese Schritte kombiniert haben, weist der Komponententest den folgenden Code auf:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace VSSample.Tests
{
    using System;
    using System.Net;
    using System.Net.Http;
    using System.Text;
    using System.Threading.Tasks;
    using System.Net.Http.Headers;
    using Microsoft.Azure.WebJobs.Extensions.DurableTask;
    using Microsoft.Extensions.Logging;
    using Moq;
    using Xunit;

    public class HttpStartTests
    {
        [Fact]
        public async Task HttpStart_returns_retryafter_header()
        {
            // Define constants
            const string functionName = "SampleFunction";
            const string instanceId = "7E467BDB-213F-407A-B86A-1954053D3C24";

            // Mock TraceWriter
            var loggerMock = new Mock<ILogger>();

            // Mock DurableOrchestrationClientBase
            var clientMock = new Mock<IDurableClient>();

            // Mock StartNewAsync method
            clientMock.
                Setup(x => x.StartNewAsync(functionName, It.IsAny<string>(), It.IsAny<object>())).
                ReturnsAsync(instanceId);

            // Mock CreateCheckStatusResponse method
            clientMock
                .Setup(x => x.CreateCheckStatusResponse(It.IsAny<HttpRequestMessage>(), instanceId, false))
                .Returns(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(string.Empty),
                    Headers =
                    {
                        RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10))
                    }
                });

            // Call Orchestration trigger function
            var result = await HttpStart.Run(
                new HttpRequestMessage()
                {
                    Content = new StringContent("{}", Encoding.UTF8, "application/json"),
                    RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
                },
                clientMock.Object,
                functionName,
                loggerMock.Object);

            // Validate that output is not null
            Assert.NotNull(result.Headers.RetryAfter);

            // Validate output's Retry-After header value
            Assert.Equal(TimeSpan.FromSeconds(10), result.Headers.RetryAfter.Delta);
        }
    }
}

Orchestratorfunktionen für Komponententests

Orchestratorfunktionen sind für Komponententests noch interessanter, da sie in der Regel viel mehr Geschäftslogik haben.

In diesem Abschnitt überprüfen die Komponententests die Ausgabe der E1_HelloSequence Orchestrator-Funktion:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;

namespace VSSample
{
    public static class HelloSequence
    {
        [FunctionName("E1_HelloSequence")]
        public static async Task<List<string>> Run(
            [OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var outputs = new List<string>();

            outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Tokyo"));
            outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Seattle"));
            outputs.Add(await context.CallActivityAsync<string>("E1_SayHello_DirectInput", "London"));

            // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
            return outputs;
        }

        [FunctionName("E1_SayHello")]
        public static string SayHello([ActivityTrigger] IDurableActivityContext context)
        {
            string name = context.GetInput<string>();
            return $"Hello {name}!";
        }

        [FunctionName("E1_SayHello_DirectInput")]
        public static string SayHelloDirectInput([ActivityTrigger] string name)
        {
            return $"Hello {name}!";
        }
    }
 }

Der Komponententestcode beginnt mit dem Erstellen eines Modells:

var durableOrchestrationContextMock = new Mock<IDurableOrchestrationContext>();

Dann werden die Aktivitätsmethodenaufrufe simuliert:

durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "London")).ReturnsAsync("Hello London!");

Als Nächstes ruft der Komponententest die HelloSequence.Run Methode auf:

var result = await HelloSequence.Run(durableOrchestrationContextMock.Object);

Und schließlich wird die Ausgabe überprüft:

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

Nachdem Sie die vorherigen Schritte kombiniert haben, weist der Komponententest den folgenden Code auf:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace VSSample.Tests
{
    using System.Threading.Tasks;
    using Microsoft.Azure.WebJobs.Extensions.DurableTask;
    using Moq;
    using Xunit;

    public class HelloSequenceTests
    {
        [Fact]
        public async Task Run_returns_multiple_greetings()
        {
            var mockContext = new Mock<IDurableOrchestrationContext>();
            mockContext.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
            mockContext.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
            mockContext.Setup(x => x.CallActivityAsync<string>("E1_SayHello_DirectInput", "London")).ReturnsAsync("Hello London!");

            var result = await HelloSequence.Run(mockContext.Object);

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

Aktivitätsfunktionen für Komponententests

Aktivitätsfunktionen werden auf die gleiche Weise getestet wie nicht dauerhafte Funktionen.

In diesem Abschnitt überprüft der Komponententest das Verhalten der E1_SayHello Aktivitätsfunktion:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;

namespace VSSample
{
    public static class HelloSequence
    {
        [FunctionName("E1_HelloSequence")]
        public static async Task<List<string>> Run(
            [OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var outputs = new List<string>();

            outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Tokyo"));
            outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Seattle"));
            outputs.Add(await context.CallActivityAsync<string>("E1_SayHello_DirectInput", "London"));

            // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
            return outputs;
        }

        [FunctionName("E1_SayHello")]
        public static string SayHello([ActivityTrigger] IDurableActivityContext context)
        {
            string name = context.GetInput<string>();
            return $"Hello {name}!";
        }

        [FunctionName("E1_SayHello_DirectInput")]
        public static string SayHelloDirectInput([ActivityTrigger] string name)
        {
            return $"Hello {name}!";
        }
    }
 }

Und die Komponententests überprüfen das Format der Ausgabe. Diese Unit-Tests verwenden entweder die Parametertypen direkt oder simulieren die IDurableActivityContext Klasse.

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace VSSample.Tests
{
    using Microsoft.Azure.WebJobs.Extensions.DurableTask;
    using Xunit;
    using Moq;

    public class HelloSequenceActivityTests
    {
        [Fact]
        public void SayHello_returns_greeting()
        {
            var durableActivityContextMock = new Mock<IDurableActivityContext>();
            durableActivityContextMock.Setup(x => x.GetInput<string>()).Returns("John");
            var result = HelloSequence.SayHello(durableActivityContextMock.Object);
            Assert.Equal("Hello John!", result);
        }

        [Fact]
        public void SayHello_returns_greeting_direct_input()
        {
            var result = HelloSequence.SayHelloDirectInput("John");
            Assert.Equal("Hello John!", result);
        }
    }
}

Nächste Schritte