ทดสอบยูนิตเอเจนต์ของคุณใน .NET

การทดสอบหน่วยช่วยให้คุณตรวจสอบการทำงานของเอเจนต์ได้ในแต่ละเทิร์น โดยไม่ต้องปรับใช้กับแชนเนล เชื่อมต่อกับ Azure AI Bot Service หรือส่งทราฟฟิก HTTP จริง คู่มือนี้สร้างตั้งแต่การทดสอบเสียงสะท้อนแบบง่ายไปจนถึงโฟลว์แบบหลายรอบ การจำลองการขึ้นต่อกัน และการตรวจสอบเชิงความหมายที่รองรับด้วย AI

Prerequisites

เพิ่มแพคเกจ Microsoft.Agents.Builder.Testing NuGet ไปยังโครงการทดสอบของคุณ ซึ่งรวมถึง AgentTestHost, TestAdapterและ TestFlow - สามคอมโพเนนต์ที่คุณใช้ในการทดสอบทุกครั้ง

<PackageReference Include="Microsoft.Agents.Builder.Testing" Version="*" />

การทดสอบในคู่มือนี้ใช้ xUnit และ Moq แต่ไลบรารีการทดสอบไม่ขึ้นอยู่กับเฟรมเวิร์กการทดสอบเฉพาะ

แนวคิดหลัก

TestAdapter

TestAdapter เป็นตัวแทนชั่วคราวที่ทำงานอยู่ภายในโพรเซสสำหรับอะแด็ปเตอร์ช่องสัญญาณจริง แทนที่จะส่งกิจกรรมผ่านเครือข่าย ระบบจะจัดคิวการตอบกลับของเอเจนต์ไว้ใน ActiveQueue ซึ่งคำสั่งตรวจสอบของคุณจะอ่านจากตรงนั้น นอกจากนี้ยังมี MockUserTokenClient สำหรับจำลองโฟลว์ OAuth

TestFlow

TestFlow เป็น API คล่องแคล่วสําหรับการสร้างแบบจําลองการสนทนาเป็นลําดับการดําเนินการส่ง/assert การทำงานแบบลูกโซ่นี้จะถูกประมวลผลเมื่อจำเป็นต้องใช้เท่านั้น ไม่มีอะไรดําเนินการจนกว่าคุณจะเรียกStartTestAsync()

await new TestFlow(adapter, myBotCallback)
    .Send("hello")
    .AssertReply("Hello back!")
    .StartTestAsync();

AgentTestHost

AgentTestHost ครอบ IHost ที่กำหนดค่าในลักษณะเดียวกับ Program.cs สำหรับการทำงานจริง โดยจะลงทะเบียน TestAdapter ไว้ล่วงหน้าเป็น IChannelAdapter และเปิดให้เข้าถึง CreateTestFlow() เพื่อเริ่มการสนทนาทดสอบ

await using var host = AgentTestHost.Create(builder =>
{
    builder.Services.AddSingleton<IStorage, MemoryStorage>();
    builder.Services.AddTransient<IAgent, MyAgent>();
});

await host.CreateTestFlow()
    .Send("hello")
    .AssertReply("Hello back!")
    .StartTestAsync();

สำคัญ: อย่าเรียกAddAgent<T>()ภายในAgentTestHost.Create() เมธอดนั้นลงทะเบียน CloudAdapter ซึ่งขัดแย้งกับ TestAdapter ที่ลงทะเบียนไว้ล่วงหน้าแล้ว ลงทะเบียน IAgent โดยตรงโดยใช้ AddTransient<IAgent, T>() หรือตัวแทนโรงงาน

การทดสอบครั้งแรกของคุณ

กําหนดตัวแทนที่สะท้อนข้อความของผู้ใช้:

public class EchoAgent : AgentApplication
{
    public EchoAgent(AgentApplicationOptions options) : base(options)
    {
        OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last);
    }

    private async Task OnMessageAsync(ITurnContext tc, ITurnState ts, CancellationToken ct)
    {
        await tc.SendActivityAsync($"You said: {tc.Activity.Text}", cancellationToken: ct);
    }
}

การทดสอบ:

[Fact]
public async Task Echo_ReturnsUserText()
{
    await using var host = AgentTestHost.Create(builder =>
    {
        builder.Services.AddSingleton<IStorage, MemoryStorage>();
        builder.Services.AddTransient<IAgent>(sp =>
            new EchoAgent(new AgentApplicationOptions(sp.GetRequiredService<IStorage>())));
    });

    await host.CreateTestFlow()
        .Send("hello")
        .AssertReply("You said: hello")
        .StartTestAsync();
}

แต่ละขั้นตอนทําอะไรได้บ้าง

ขั้นตอน Description
AgentTestHost.Create(...) สร้างและเริ่มต้นโฮสต์ภายในโปรเซสโดยมี TestAdapter ที่กำหนดค่าไว้ล่วงหน้า
CreateTestFlow() ดึง IAgent จาก DI และครอบไว้ใน TestFlow
.Send("hello") Messageสร้างกิจกรรมและกําหนดเส้นทางกิจกรรมผ่านตัวแทน
.AssertReply("You said: hello") นำข้อความตอบกลับถัดไปออกจากคิวและยืนยันข้อความ (ถูกตัดออก, ตรงกันทุกประการ)
.StartTestAsync() ประมวลผลคำสั่งในห่วงโซ่ทั้งหมด

ตัวแปรการยืนยัน

TestFlow มีวิธีการยืนยันหลายวิธีโดยขึ้นอยู่กับความแม่นยําในการตรวจสอบของคุณ

จับคู่ข้อความตรงกันทุกประการ

.AssertReply("You said: hello")

การจับคู่สตริงย่อย

.AssertReplyContains("hello")

หนึ่งในคําตอบที่ยอมรับได้

.AssertReplyOneOf(new[] { "Hi!", "Hello!", "Hey!" })

การตรวจสอบแบบกําหนดเองกับผู้รับมอบสิทธิ์

.AssertReply(activity =>
{
    Assert.Equal(ActivityTypes.Message, activity.Type);
    Assert.Contains("hello", activity.Text);
})

ผู้รับมอบสิทธิ์การตรวจสอบ Async

.AssertReplySatisfies(async activity =>
{
    var data = await LoadExpectedDataAsync();
    Assert.Equal(data.Expected, activity.Text);
})

ยืนยันว่าไม่มีการตอบกลับเพิ่มเติม

หลังจากการยืนยันการตอบกลับที่คาดไว้ทั้งหมดแล้วให้ป้องกันส่วนเกินที่ไม่คาดคิด:

.AssertReply("done")
.AssertNoMoreReplies()

AssertNoMoreReplies() รอ 1 วินาทีและล้มเหลวหากกิจกรรมใด ๆ มาถึง ใช้พารามิเตอร์คําอธิบายเพื่อตั้งชื่อการยืนยันในผลลัพธ์ที่ล้มเหลว:

.AssertNoMoreReplies("only one reply expected per turn")

รวมการส่งและการตรวจสอบด้วย Test()

สำหรับคู่แบบเทิร์นเดียว .Test()จะรวม .Send() และ .AssertReply():

await host.CreateTestFlow()
    .Test("ping", "pong")
    .Test("foo",  "You said: foo")
    .StartTestAsync();

การสนทนาแบบหลายรอบ

เชื่อมคู่คำสั่ง send/assert หลายคู่เข้าด้วยกันเพื่อทดสอบลำดับการสนทนาแบบครบถ้วน:

await host.CreateTestFlow()
    .Send("start")
    .AssertReply("What is your name?")
    .Send("Alice")
    .AssertReply("Hello, Alice!")
    .Send("what can you do?")
    .AssertReply(activity => Assert.NotEmpty(activity.Text))
    .StartTestAsync();

ทดสอบการตอบกลับหลายรายการต่อหนึ่งรอบ

ตัวแทนสามารถส่งหลายกิจกรรมในการตอบสนองต่อข้อความของผู้ใช้หนึ่ง การเรียกใช้แต่ละครั้ง .AssertReply() จะส่งผลกับกิจกรรมหนึ่งรายการจากคิว:

await host.CreateTestFlow()
    .Send("tell me three things")
    .AssertReply("First thing.")
    .AssertReply("Second thing.")
    .AssertReply("Third thing.")
    .AssertNoMoreReplies()
    .StartTestAsync();

ทดสอบการอัปเดตการสนทนา

ช่องใช้กิจกรรม ConversationUpdate เพื่อแจ้งให้เอเจนต์ทราบเมื่อสมาชิกเข้าร่วมหรือออกจากช่อง ใช้ SendConversationUpdate() เพื่อจําลองเหตุการณ์การรวมที่ทริกเกอร์ข้อความต้อนรับ

ทดสอบกับผู้ใช้เริ่มต้น

ถ้าคุณไม่ส่งผ่านอาร์กิวเมนต์ใดก็ตาม อะแด็ปเตอร์จะเพิ่มผู้ใช้เริ่มต้น (user1) เป็น MembersAdded:

await host.CreateTestFlow()
    .SendConversationUpdate()
    .AssertReply("Hello and Welcome!")
    .StartTestAsync();

ทดสอบกับสมาชิกที่เฉพาะเจาะจง

await host.CreateTestFlow()
    .SendConversationUpdate(new[]
    {
        new ChannelAccount { Id = "alice", Name = "Alice" },
        new ChannelAccount { Id = "bob",   Name = "Bob"   }
    })
    .AssertReply("Hello and Welcome!")
    .StartTestAsync();

ขั้นตอนการต้อนรับและลำดับข้อความแบบเต็ม

await host.CreateTestFlow()
    .SendConversationUpdate()
    .AssertReply("Hello and Welcome!")
    .Send("hello")
    .AssertReply("You said: hello")
    .AssertNoMoreReplies()
    .StartTestAsync();

ตรวจสอบกิจกรรมเต็มรูปแบบ

เมื่อคุณต้องการตรวจสอบคุณสมบัตินอกเหนือจาก Textเช่น Speak, , InputHintSuggestedActionsหรือสิ่งที่แนบมา ให้ใช้การยืนยันผู้รับมอบสิทธิ์:

.AssertReply(activity =>
{
    Assert.Equal("Sure, one moment…", activity.Text);
    Assert.Equal("Sure, one moment…", activity.Speak);
    Assert.Equal(InputHints.IgnoringInput, activity.InputHint);
})

สําหรับไฟล์แนบของการ์ดที่ปรับเปลี่ยนได้ TestAdapter ดำเนินการจัดเรียงแบบอนุกรมแบบไปกลับ ดังนั้น Attachment.Content จะถูกส่งกลับมาเป็น JsonElement ใช้ .ToString() เพื่อแยกค่าสตริง:

.AssertReply(activity =>
{
    var card = activity.Attachments
        .First(a => a.ContentType == ContentTypes.AdaptiveCard);

    // Correct: Content is JsonElement after TestAdapter serialization
    string json = card.Content.ToString()!;
    Assert.Contains("Seattle", json);
})

ตัวบ่งชี้การพิมพ์

หากเจ้าหน้าที่ของคุณส่งกิจกรรมการพิมพ์ก่อนการตอบกลับจริง ให้ยืนยันอย่างชัดเจน:

.Send("hello")
.AssertTypingIndicator()
.AssertReply("Here is your answer…")

กําหนดชนิดกิจกรรม

.AssertReply(activity =>
{
    Assert.Equal(ActivityTypes.Message, activity.Type);
})

การทดสอบที่ขับเคลื่อนด้วยข้อมูล

แทนที่จะเขียนการทดสอบแยกต่างหากสําหรับแต่ละตัวแปรการป้อนข้อมูล ให้ใช้การทดสอบ xUnit Theory ด้วย [InlineData] เพื่อกําหนดพารามิเตอร์:

[Theory]
[InlineData("hi",    "You said: hi")]
[InlineData("hello", "You said: hello")]
[InlineData("bye",   "You said: bye")]
public async Task Echo_VariousInputs(string input, string expected)
{
    await using var host = AgentTestHost.Create(builder =>
    {
        builder.Services.AddSingleton<IStorage, MemoryStorage>();
        builder.Services.AddTransient<IAgent>(sp =>
            new EchoAgent(new AgentApplicationOptions(sp.GetRequiredService<IStorage>())));
    });

    await host.CreateTestFlow()
        .Test(input, expected)
        .StartTestAsync();
}

สําหรับกรณีการทดสอบที่ซับซ้อนรวมถึงหลายรอบและสถานะการสนทนาที่แตกต่างกันให้ใช้ [MemberData]:

public static IEnumerable<object[]> ConversationFlows()
{
    yield return new object[]
    {
        new string[] { "start", "Alice", "done" },
        new string[] { "What is your name?", "Hello, Alice!", "Goodbye, Alice!" }
    };
    yield return new object[]
    {
        new string[] { "start", "Bob", "done" },
        new string[] { "What is your name?", "Hello, Bob!", "Goodbye, Bob!" }
    };
}

[Theory]
[MemberData(nameof(ConversationFlows))]
public async Task MultiTurn_Flow(string[] inputs, string[] expected)
{
    await using var host = AgentTestHost.Create(builder =>
    {
        builder.Services.AddSingleton<IStorage, MemoryStorage>();
        builder.Services.AddTransient<IAgent, GreetingAgent>();
    });

    var flow = host.CreateTestFlow();
    for (int i = 0; i < inputs.Length; i++)
    {
        flow = flow.Test(inputs[i], expected[i]);
    }
    await flow.StartTestAsync();
}

ใช้ตัวอย่างจําลอง

การทดสอบหน่วยควรทำให้คอมโพเนนต์ที่กำลังทดสอบทำงานอย่างเป็นอิสระ ใช้ Moq เพื่อแทนที่การขึ้นต่อกันภายนอก เช่น บริการ AI ฐานข้อมูล และตัวรู้จํา เพื่อให้การทดสอบยังคงเป็นแบบกําหนดและรวดเร็ว

ส่งการจำลองการขึ้นต่อกันเข้าสู่ระบบ

ออกแบบเอเจนต์ของคุณให้รับการอ้างอิงที่จำเป็นผ่านคอนสตรักเตอร์ แทนที่จะสร้างเองภายใน:

// Harder to test — agent owns the recognizer
public class WeatherAgent : AgentApplication
{
    private readonly WeatherService _weather = new WeatherService();
}

// Easier to test — dependency injected
public class WeatherAgent : AgentApplication
{
    private readonly IWeatherService _weather;

    public WeatherAgent(AgentApplicationOptions options, IWeatherService weather)
        : base(options)
    {
        _weather = weather;
    }
}

แทนที่บริการในการทดสอบ

[Fact]
public async Task Weather_ReturnsFormattedForecast()
{
    var mockWeather = new Mock<IWeatherService>();
    mockWeather
        .Setup(s => s.GetForecastAsync("Seattle", It.IsAny<CancellationToken>()))
        .ReturnsAsync(new Forecast { TemperatureC = 18, Summary = "Partly cloudy" });

    await using var host = AgentTestHost.Create(builder =>
    {
        builder.Services.AddSingleton<IStorage, MemoryStorage>();
        builder.Services.AddSingleton<IWeatherService>(mockWeather.Object);
        builder.Services.AddTransient<IAgent>(sp =>
            new WeatherAgent(
                new AgentApplicationOptions(sp.GetRequiredService<IStorage>()),
                sp.GetRequiredService<IWeatherService>()));
    });

    await host.CreateTestFlow()
        .Send("weather in Seattle")
        .AssertReplyContains("18")
        .StartTestAsync();
}

ตรวจสอบการเรียกใช้บริการ

mockWeather.Verify(
    s => s.GetForecastAsync("Seattle", It.IsAny<CancellationToken>()),
    Times.Once);

การตรวจสอบความถูกต้องเชิงความหมายโดยใช้ AI

เมื่อเอเจนต์ของคุณเรียกใช้โมเดลภาษา และผลลัพธ์ของมันไม่เป็นแบบกำหนดแน่นอน การยืนยันโดยเทียบข้อความแบบตรงตัวจะเปราะบาง SemanticValidator ใช้ผู้พิพากษา AI เพื่อประเมินว่าการตอบกลับนั้นสอดคล้องกับคําถามใช่/ไม่ใช่หรือไม่

SemanticValidator

var validator = new SemanticValidator(chatClient, "Does this response describe the weather?");

await host.CreateTestFlow()
    .Send("What is the weather in Seattle?")
    .AssertReplySatisfies(validator, timeout: 60_000)
    .StartTestAsync();

ผู้ตรวจสอบความถูกต้องส่งข้อความตอบกลับของเจ้าหน้าที่ไปยังผู้พิพากษา AI พร้อมคําถามยืนยัน ถ้าผู้พิพากษาตอบว่า "ใช่" การยืนยันก็ผ่านไป หากระบบตอบกลับว่า "ไม่" การทดสอบจะถือว่าล้มเหลว พร้อมข้อความโดยละเอียดซึ่งแสดงพรอมต์และคำตอบที่ได้รับจริง

สร้าง IChatClient

using Azure.AI.OpenAI;
using Azure.Core;
using Microsoft.Extensions.AI;

IChatClient chatClient = new AzureOpenAIClient(
        new Uri(endpoint),
        new ApiKeyCredential(apiKey))   // ApiKeyCredential is from System.ClientModel
    .GetChatClient(deploymentName)
    .AsIChatClient();

IResponseValidator แบบกําหนดเอง

นําไปใช้ IResponseValidator เมื่อ SemanticValidator ไม่พอดี ตัวอย่างเช่น เมื่อการตอบกลับเป็นอะแดปทีฟการ์ดแทนที่จะเป็นข้อความธรรมดา:

public class WeatherCardValidator : IResponseValidator
{
    private readonly IChatClient _judge;
    private readonly string _location;

    public WeatherCardValidator(IChatClient judge, string location)
    {
        _judge = judge;
        _location = location;
    }

    public async Task ValidateAsync(IActivity reply, CancellationToken ct = default)
    {
        string content;

        if (!string.IsNullOrEmpty(reply?.Text))
        {
            content = reply.Text;
        }
        else
        {
            var card = reply?.Attachments?
                .FirstOrDefault(a => a.ContentType == ContentTypes.AdaptiveCard);

            if (card?.Content == null)
                throw new InvalidOperationException("Reply has neither text nor Adaptive Card.");

            // Content is JsonElement after TestAdapter round-trip — use ToString()
            content = card.Content.ToString()!;
        }

        var messages = new List<ChatMessage>
        {
            new(ChatRole.System, "Answer only 'yes' or 'no'."),
            new(ChatRole.User, $"Does this contain a weather forecast for {_location}?\n\n{content}")
        };

        var response = await _judge.GetResponseAsync(messages, cancellationToken: ct);
        var answer = response?.Text?.Trim().ToLowerInvariant() ?? string.Empty;

        if (answer.StartsWith("yes"))
            return;

        if (answer.StartsWith("no"))
            throw new InvalidOperationException(
                $"Validation failed for '{_location}'. Agent replied:\n{content}");

        throw new InvalidOperationException($"Unexpected judge response: '{answer}'");
    }
}

ใช้:

[Fact(Skip = "Requires Azure OpenAI. Set AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_KEY, AZURE_OPENAI_DEPLOYMENT_NAME.")]
public async Task WeatherAgent_ReturnsSeattleForecast()
{
    string endpoint   = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!;
    string apiKey     = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!;
    string deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")!;

    IChatClient judge = new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(apiKey))
        .GetChatClient(deployment)
        .AsIChatClient();

    var validator = new WeatherCardValidator(judge, "Seattle");

    await using var host = AgentTestHost.Create(builder =>
    {
        builder.Services.AddSingleton<IStorage, MemoryStorage>();
        // ... register Kernel and agent
    });

    await host.CreateTestFlow()
        .SendConversationUpdate()
        .AssertReplyContains("Welcome")
        .Send("What is the weather forecast for Seattle today?")
        .AssertReplySatisfies(validator, timeout: 60_000)
        .AssertNoMoreReplies()
        .StartTestAsync();
}

Note

การทดสอบที่เรียกใช้บริการ AI ภายนอกทำงานช้าและขึ้นอยู่กับสภาพแวดล้อม ทำเครื่องหมายด้วย [Fact(Skip = "...")] และข้อความที่ชัดเจนเกี่ยวกับตัวแปรสภาพแวดล้อมที่จำเป็น เรียกใช้สิ่งเหล่านั้นในขั้นตอน CI ที่แยกไว้เฉพาะ แทนที่จะรันร่วมกับชุดการทดสอบยูนิตของคุณ

ทดสอบมิดเดิลแวร์

แนบมิดเดิลแวร์กับ host.Adapter ก่อนที่จะสร้างโฟลว์ทดสอบ โฟลว์ทั้งหมดถูกสร้างขึ้นจากกิจกรรมของโพรเซสโฮสต์นั้นผ่านมิดเดิลแวร์

การบันทึกทรานสคริปต์

[Fact]
public async Task Transcript_CapturesAllActivities()
{
    await using var host = AgentTestHost.Create(builder =>
    {
        builder.Services.AddSingleton<IStorage, MemoryStorage>();
        builder.Services.AddTransient<IAgent, EchoAgent>();
    });

    var transcript = new MemoryTranscriptStore();
    host.Adapter.Use(new TranscriptLoggerMiddleware(transcript));

    await host.CreateTestFlow()
        .SendConversationUpdate()
        .AssertReply("Hello and Welcome!")
        .Send("hello")
        .AssertReply("You said: hello")
        .AssertNoMoreReplies()
        .StartTestAsync();

    var activities = await GetTranscriptAsync(
        host.Adapter.Conversation.ChannelId,
        host.Adapter.Conversation.Conversation.Id,
        transcript);

    // ConversationUpdate in, welcome out, message in, echo out = 4
    Assert.Equal(4, activities.Count);
}

private static async Task<IList<IActivity>> GetTranscriptAsync(
    string channelId, string conversationId, ITranscriptStore store)
{
    var result = new List<IActivity>();
    string token = null;
    do
    {
        var page = await store.GetTranscriptActivitiesAsync(channelId, conversationId, token);
        token = page.ContinuationToken;
        result.AddRange(page.Items);
    } while (token != null);
    return result;
}

ใช้ TestAdapter โดยตรง

ใช้ AgentTestHost สำหรับเส้นทางการตั้งค่าที่แนะนำ อย่างไรก็ตาม คุณสามารถสร้าง TestAdapter และ TestFlow ทําได้โดยตรงเมื่อคุณต้องการตัวควบคุมระดับต่ํากว่า หรือกําลังทดสอบการเรียกกลับแบบสแตนด์อโลน

[Fact]
public async Task DirectAdapter_Echo()
{
    var adapter = TestAdapter.Create(channelId: Channels.Msteams);

    async Task BotCallback(ITurnContext tc, CancellationToken ct)
    {
        if (tc.Activity.Type == ActivityTypes.Message)
            await tc.SendActivityAsync($"echo:{tc.Activity.Text}", cancellationToken: ct);
    }

    await new TestFlow(adapter, BotCallback)
        .Test("foo", "echo:foo")
        .StartTestAsync();
}

กําหนดการอ้างอิงการสนทนา

var adapter = TestAdapter.Create(
    channelId:        "msteams",
    userId:           "u42",
    userName:         "Alice",
    botId:            "mybot",
    botName:          "MyBot",
    conversationId:   "conv-42",
    conversationName: "Test Room");

ตั้งค่าการอ้างอิงการสนทนาโดยตรงบนอะแด็ปเตอร์ของโฮสต์ก่อนที่จะสร้างโฟลว์:

host.Adapter.Conversation = new ConversationReference
{
    ChannelId    = Channels.Msteams,
    User         = new ChannelAccount("alice", "Alice"),
    Agent        = new ChannelAccount("bot",   "Bot"),
    Conversation = new ConversationAccount(false, "conv-1", "Conversation 1"),
    ServiceUrl   = "https://test.example.com"
};

การทดสอบ OAuth และโฟลว์โทเค็น

TestAdapter แสดงวิธีการใส่ข้อมูลโทเค็นผู้ใช้ไว้ล่วงหน้า วิธีการเหล่านี้ช่วยให้คุณทดสอบตัวแทนที่ใช้ GetUserTokenAsync หรือโฟลว์การลงชื่อเข้าระบบครั้งเดียวโดยไม่ต้องเชื่อมต่อกับผู้ให้บริการข้อมูลประจําตัวจริง

ใส่โทเค็นล่วงหน้า

host.Adapter.AddUserToken(
    connectionName: "MyOAuthConnection",
    channelId:      Channels.Test,
    userId:         "user1",
    token:          "fake-access-token");

เตรียมโทเค็นที่สามารถแลกเปลี่ยนได้สําหรับการลงชื่อเข้าระบบครั้งเดียว

host.Adapter.AddExchangeableToken(
    connectionName:  "MyOAuthConnection",
    channelId:       Channels.Test,
    userId:          "user1",
    exchangableItem: "sso-token",
    token:           "fake-access-token");

จําลองความล้มเหลวในการแลกเปลี่ยน

host.Adapter.ThrowOnExchangeRequest(
    connectionName:  "MyOAuthConnection",
    channelId:       Channels.Test,
    userId:          "user1",
    exchangableItem: "bad-sso-token");

แทรกการหน่วงเวลา

ใช้ .Delay() เมื่อเจ้าหน้าที่ของคุณมีพฤติกรรมที่พึ่งพาเวลา

await host.CreateTestFlow()
    .Send("start")
    .Delay(TimeSpan.FromSeconds(2))
    .Send("are you still there?")
    .AssertReply("Yes, I'm here.")
    .StartTestAsync();

ค่าเริ่มต้นของการหมดเวลาและการแก้ไขจุดบกพร่อง

วิธีการยืนยันทั้งหมดมีพารามิเตอร์เป็นtimeoutมิลลิวินาที โดยมีค่าเริ่มต้นเป็น3000 เมื่อคุณแนบตัวแก้ไขจุดบกพร่อง Visual Studio ระบบจะตั้งค่าการหมดเวลาโดยอัตโนมัติเป็น uint.MaxValue เพื่อให้จุดสั่งหยุดไม่ทําให้เกิดความล้มเหลวรุนแรง

สำหรับการดำเนินการของเอเจนต์ที่ใช้เวลานาน เช่น การเรียกใช้โมเดลภาษา ควรกำหนดค่า timeout อย่างชัดเจน

.AssertReplySatisfies(validator, timeout: 60_000)

AssertNoMoreReplies() ใช้ค่าเริ่มต้นที่สั้นลงของ 1000 ms เนื่องจากตัวแทนการประมวลผลเสร็จสิ้นแล้ว

การอ้างอิง

AgentTestHost

สมาชิก Description
AgentTestHost.Create(configure) สร้างและเริ่มต้นโฮสต์ ลงทะเบียน TestAdapter ล่วงหน้าเป็น IChannelAdapter
host.Adapter อินสแตนซ์TestAdapter แนบมิดเดิลแวร์ที่นี่
host.CreateTestFlow() ดึง IAgent จาก DI และส่งกลับ TestFlow
host.DisposeAsync() หยุดโฮสต์ ใช้ await using เพื่อกําจัด

TestAdapter

สมาชิก Description
TestAdapter.Create(...) ตัวสร้างสำหรับอะแดปเตอร์ที่มีการกำหนดค่าไว้อย่างสมบูรณ์
adapter.Conversation ConversationReference ใช้สำหรับทุกกิจกรรม
adapter.ActiveQueue คิวกิจกรรมที่เอเจนต์ส่ง
adapter.Use(middleware) แนบมิดเดิลแวร์ ส่งคืน this สำหรับการเกี่ยวโยง
adapter.AddUserToken(...) เตรียมโทเค็นผู้ใช้สําหรับการทดสอบ OAuth
adapter.AddExchangeableToken(...) กรอกโทเค็นที่แลกเปลี่ยนได้สำหรับ SSO ล่วงหน้า
adapter.ThrowOnExchangeRequest(...) กําหนดค่าคําขอ Exchange ที่จะส่ง

วิธี TestFlow

วิธีการ Description
.Send(text) ส่งกิจกรรมประเภทข้อความ
.Send(activity) ส่งประเภทกิจกรรมใดก็ได้
.SendConversationUpdate() ส่ง ConversationUpdate ให้กับผู้ใช้เริ่มต้น
.SendConversationUpdate(members) ส่ง ConversationUpdate ให้กับสมาชิกที่เฉพาะเจาะจง
.Test(input, expected) .Send() + .AssertReply() แบบย่อ
.AssertReply(string) การตรงกันของข้อความแบบตรงกันทุกประการ (ตัดช่องว่าง)
.AssertReplyContains(string) การจับคู่สตริงย่อย
.AssertReplyOneOf(string[]) จับคู่สตริงใดสตริงหนึ่งจากหลายสตริง
.AssertReply(Activity) จับคู่ตามชนิดกิจกรรมและข้อความ
.AssertReply(Action<IActivity>) การตรวจสอบการซิงค์แบบกําหนดเอง ส่งเป็นล้มเหลว
.AssertReplySatisfies(Func<IActivity, Task>) การตรวจสอบความถูกต้องแบบอะซิงก์ที่กำหนดเอง
.AssertReplySatisfies(IResponseValidator) มอบหมายไปยังวัตถุตัวตรวจสอบ
.AssertTypingIndicator() ต้องมีกิจกรรมการพิมพ์
.AssertNoMoreReplies() ล้มเหลวหากกิจกรรมใด ๆ มาถึงภายใน 1 วินาที
.Delay(ms) แทรกการหยุดชั่วคราวในโฟลว์
.StartTestAsync() ประมวลผลคำสั่งในห่วงโซ่

SemanticValidator

สมาชิก Description
new SemanticValidator(chatClient, prompt) สร้างตัวตรวจสอบ prompt คือคําถามใช่/ไม่ใช่เกี่ยวกับการตอบกลับ
ValidateAsync(activity, ct) ถือว่าผ่านหาก AI ตอบว่า "ใช่"; จะเกิดข้อยกเว้นหากตอบว่า "ไม่" หรือคำตอบอื่นที่ไม่คาดคิด