Bagikan melalui


Pengujian unit Durable Functions (C#)

Pengujian unit adalah bagian penting dari praktik pengembangan perangkat lunak modern. Pengujian unit memverifikasi perilaku logika bisnis dan melindungi dari perubahan yang melanggar tanpa disadari di masa mendatang. Durable Functions dapat dengan mudah tumbuh dalam kompleksitas sehingga memperkenalkan pengujian unit akan membantu menghindari perubahan yang tidak diinginkan. Bagian berikut menjelaskan cara menyatukan uji tiga jenis fungsi - Fungsi klien orkestrasi, orkestrator, dan aktivitas.

Catatan

Artikel ini menyediakan panduan untuk pengujian unit untuk aplikasi Durable Functions yang ditulis dalam C# untuk pekerja dalam proses .NET dan menargetkan Durable Functions 2.x. Untuk mengetahui informasi selengkapnya tentang perbedaan antara versi, lihat artikel Versi Durable Functions.

Prasyarat

Contoh dalam artikel ini memerlukan pengetahuan tentang konsep dan kerangka kerja berikut:

  • Pengujian Unit

  • Fungsi Tahan Lama

  • xUnit - Kerangka kerja pengujian

  • moq - Kerangka kerja peniruan

Kelas dasar untuk peniruan

Peniruan didukung melalui antarmuka berikut:

Antarmuka ini dapat digunakan dengan berbagai pemicu dan pengikatan yang didukung oleh Durable Functions. Saat menjalankan Azure Functions Anda, runtime fungsi akan menjalankan kode fungsi dengan implementasi konkret antarmuka ini. Untuk pengujian unit, Anda dapat meneruskan versi tiruan antarmuka ini untuk menguji logika bisnis.

Fungsi pemicu pengujian unit

Di bagian ini, pengujian unit akan memvalidasi logika fungsi pemicu HTTP berikut untuk memulai orkestrasi baru.

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

Tugas pengujian unit adalah memverifikasi nilai Retry-After header yang disediakan dalam payload respons. Jadi tes unit akan meniru beberapa metode IDurableClient untuk memastikan perilaku yang dapat diprediksi.

Pertama, kita menggunakan kerangka kerja peniruan (moq dalam kasus ini) untuk meniru IDurableClient:

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

Catatan

Meskipun Anda dapat meniru antarmuka dengan langsung mengimplementasikan antarmuka sebagai kelas, kerangka kerja peniruan menyederhanakan proses dengan berbagai cara. Misalnya, jika metode baru ditambahkan ke antarmuka di seluruh rilisan minor, moq tidak akan memerlukan perubahan kode apa pun tidak seperti implementasi konkret.

Kemudian StartNewAsync metode ditiru untuk mengembalikan ID instans terkenal.

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

Selanjutnya CreateCheckStatusResponse ditiru untuk selalu mengembalikan respons HTTP 200 kosong.

// 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 juga ditiru:

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

Sekarang metode Run dipanggil dari tes unit:

// 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);

Langkah terakhir adalah membandingkan output dengan nilai yang diharapkan:

// 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);

Setelah menggabungkan semua langkah, tes unit akan memiliki kode berikut:

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

Fungsi orkestrator pengujian unit

Fungsi orkestrator bahkan lebih menarik untuk pengujian unit karena mereka biasanya memiliki lebih banyak logika bisnis.

Di bagian ini tes unit akan memvalidasi output fungsi E1_HelloSequence Orchestrator:

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

Kode tes unit akan dimulai dengan membuat tiruan:

var durableOrchestrationContextMock = new Mock<IDurableOrchestrationContext>();

Kemudian panggilan metode aktivitas akan ditiru:

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

Selanjutnya tes unit akan memanggil metode HelloSequence.Run:

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

Dan akhirnya output akan divalidasi:

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

Setelah menggabungkan semua langkah, tes unit akan memiliki kode berikut:

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

Fungsi aktivitas pengujian unit

Fungsi aktivitas dapat berupa unit yang diuji dengan cara yang sama seperti fungsi yang tidak tahan lama.

Di bagian ini tes unit akan memvalidasi perilaku E1_SayHello fungsi Aktivitas:

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

Dan tes unit akan memverifikasi format output. Tes unit dapat menggunakan jenis parameter secara langsung atau meniru IDurableActivityContext kelas:

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

Langkah berikutnya