AgentFramework:测试方法

概述

测试是确保 AI 代理应用质量的关键。本文将介绍单元测试和集成测试的方法,并提供实用的测试代码示例。

为什么测试很重要?

想象一下,如果你的 AI 客服在上线后才发现无法正确回答用户问题,或者工具调用失败,会造成多大的损失?

测试就像给你的应用做"体检",在问题发生之前就发现并修复它们。

测试的好处

  1. 提前发现问题:在开发阶段就发现 bug

  2. 保证质量:确保功能按预期工作

  3. 安全重构:修改代码时不怕破坏现有功能

  4. 文档作用:测试代码展示了如何使用功能

测试的类型

1. 单元测试

测试单个函数或类的功能,不依赖外部服务。

2. 集成测试

测试多个组件协同工作,包括与外部服务的交互。

3. 端到端测试

测试完整的用户场景,从输入到输出。

单元测试

1. 测试框架选择

在 .NET 中,常用的测试框架有:

  • xUnit:现代、简洁(推荐)

  • NUnit:功能丰富

  • MSTest:微软官方

本文使用 xUnit 作为示例。

2. 创建测试项目

复制代码
# 创建测试项目
dotnet new xunit -n AgentApp.Tests

# 添加项目引用
cd AgentApp.Tests
dotnet add reference ../AgentApp/AgentApp.csproj

# 添加必要的包
dotnet add package Moq
dotnet add package FluentAssertions

3. 测试工具函数

假设我们有一个数据脱敏工具:

复制代码
// AgentApp/DataMasker.cs
public class DataMasker
{
    public string MaskPhoneNumber(string text)
    {
        return Regex.Replace(text, @"1[3-9]\d{9}", m =>
        {
            var phone = m.Value;
            return phone.Substring(0, 3) + "****" + phone.Substring(7);
        });
    }
    
    public string MaskEmail(string text)
    {
        return Regex.Replace(text, @"[\w\.-]+@[\w\.-]+\.\w+", m =>
        {
            var email = m.Value;
            var parts = email.Split('@');
            if (parts[0].Length <= 2)
                return "***@" + parts[1];
            return parts[0].Substring(0, 2) + "***@" + parts[1];
        });
    }
}

单元测试

复制代码
// AgentApp.Tests/DataMaskerTests.cs
using Xunit;
using FluentAssertions;

public class DataMaskerTests
{
    private readonly DataMasker _masker;
    
    public DataMaskerTests()
    {
        _masker = new DataMasker();
    }
    
    [Fact]
    public void MaskPhoneNumber_ShouldMaskMiddleDigits()
    {
        // Arrange
        var input = "我的手机号是13812345678";
        
        // Act
        var result = _masker.MaskPhoneNumber(input);
        
        // Assert
        result.Should().Be("我的手机号是138****5678");
    }
    
    [Theory]
    [InlineData("13812345678", "138****5678")]
    [InlineData("15912345678", "159****5678")]
    [InlineData("18812345678", "188****5678")]
    public void MaskPhoneNumber_ShouldHandleVariousPhoneNumbers(string phone, string expected)
    {
        // Act
        var result = _masker.MaskPhoneNumber(phone);
        
        // Assert
        result.Should().Be(expected);
    }
    
    [Fact]
    public void MaskEmail_ShouldMaskUsername()
    {
        // Arrange
        var input = "联系我:zhangsan@example.com";
        
        // Act
        var result = _masker.MaskEmail(input);
        
        // Assert
        result.Should().Be("联系我:zh***@example.com");
    }
    
    [Fact]
    public void MaskEmail_ShortUsername_ShouldMaskCompletely()
    {
        // Arrange
        var input = "邮箱:ab@test.com";
        
        // Act
        var result = _masker.MaskEmail(input);
        
        // Assert
        result.Should().Be("邮箱:***@test.com");
    }
}

运行测试

复制代码
dotnet test

4. 测试输入验证

复制代码
// AgentApp/InputValidator.cs
public class InputValidator
{
    private const int MaxMessageLength = 4000;
    
    public bool ValidateMessageLength(string message, out string error)
    {
        if (string.IsNullOrWhiteSpace(message))
        {
            error = "消息不能为空";
            return false;
        }
        
        if (message.Length > MaxMessageLength)
        {
            error = $"消息长度不能超过 {MaxMessageLength} 字符";
            return false;
        }
        
        error = null;
        return true;
    }
}

单元测试

复制代码
// AgentApp.Tests/InputValidatorTests.cs
public class InputValidatorTests
{
    private readonly InputValidator _validator;
    
    public InputValidatorTests()
    {
        _validator = new InputValidator();
    }
    
    [Fact]
    public void ValidateMessageLength_ValidMessage_ShouldReturnTrue()
    {
        // Arrange
        var message = "这是一条有效的消息";
        
        // Act
        var result = _validator.ValidateMessageLength(message, out var error);
        
        // Assert
        result.Should().BeTrue();
        error.Should().BeNull();
    }
    
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void ValidateMessageLength_EmptyMessage_ShouldReturnFalse(string message)
    {
        // Act
        var result = _validator.ValidateMessageLength(message, out var error);
        
        // Assert
        result.Should().BeFalse();
        error.Should().Be("消息不能为空");
    }
    
    [Fact]
    public void ValidateMessageLength_TooLongMessage_ShouldReturnFalse()
    {
        // Arrange
        var message = new string('a', 4001);
        
        // Act
        var result = _validator.ValidateMessageLength(message, out var error);
        
        // Assert
        result.Should().BeFalse();
        error.Should().Contain("不能超过");
    }
}

5. 使用 Mock 测试依赖

当测试的代码依赖外部服务时,使用 Mock 对象模拟这些依赖。

复制代码
// AgentApp/AgentService.cs
public interface IChatClient
{
    Task<string> SendMessageAsync(string message);
}

public class AgentService
{
    private readonly IChatClient _chatClient;
    private readonly InputValidator _validator;
    
    public AgentService(IChatClient chatClient, InputValidator validator)
    {
        _chatClient = chatClient;
        _validator = validator;
    }
    
    public async Task<string> ProcessMessageAsync(string message)
    {
        // 验证输入
        if (!_validator.ValidateMessageLength(message, out var error))
        {
            throw new ArgumentException(error);
        }
        
        // 调用 AI 服务
        return await _chatClient.SendMessageAsync(message);
    }
}

使用 Mock 的单元测试

复制代码
// AgentApp.Tests/AgentServiceTests.cs
using Moq;

public class AgentServiceTests
{
    private readonly Mock<IChatClient> _mockChatClient;
    private readonly InputValidator _validator;
    private readonly AgentService _service;
    
    public AgentServiceTests()
    {
        _mockChatClient = new Mock<IChatClient>();
        _validator = new InputValidator();
        _service = new AgentService(_mockChatClient.Object, _validator);
    }
    
    [Fact]
    public async Task ProcessMessageAsync_ValidMessage_ShouldCallChatClient()
    {
        // Arrange
        var message = "你好";
        var expectedResponse = "你好!我是 AI 助手";
        
        _mockChatClient
            .Setup(x => x.SendMessageAsync(message))
            .ReturnsAsync(expectedResponse);
        
        // Act
        var result = await _service.ProcessMessageAsync(message);
        
        // Assert
        result.Should().Be(expectedResponse);
        _mockChatClient.Verify(x => x.SendMessageAsync(message), Times.Once);
    }
    
    [Fact]
    public async Task ProcessMessageAsync_EmptyMessage_ShouldThrowException()
    {
        // Arrange
        var message = "";
        
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentException>(
            async () => await _service.ProcessMessageAsync(message)
        );
        
        // 验证没有调用 ChatClient
        _mockChatClient.Verify(x => x.SendMessageAsync(It.IsAny<string>()), Times.Never);
    }
}

集成测试

集成测试验证多个组件协同工作,包括与真实服务的交互。

1. 测试与 AI 服务的集成

复制代码
// AgentApp.Tests/Integration/AgentIntegrationTests.cs
using Microsoft.Extensions.Configuration;

public class AgentIntegrationTests : IDisposable
{
    private readonly ChatCompletionAgent _agent;
    private readonly IConfiguration _configuration;
    
    public AgentIntegrationTests()
    {
        // 加载配置
        _configuration = new ConfigurationBuilder()
            .AddEnvironmentVariables()
            .Build();
        
        // 创建真实的代理
        var endpoint = _configuration["AZURE_OPENAI_ENDPOINT"];
        var apiKey = _configuration["AZURE_OPENAI_API_KEY"];
        
        if (string.IsNullOrEmpty(endpoint) || string.IsNullOrEmpty(apiKey))
        {
            throw new InvalidOperationException("请配置 AZURE_OPENAI_ENDPOINT 和 AZURE_OPENAI_API_KEY");
        }
        
        var chatClient = new AzureOpenAIClient(
            new Uri(endpoint),
            new ApiKeyCredential(apiKey)
        ).GetChatClient("gpt-35-turbo");
        
        _agent = new ChatCompletionAgent(
            chatClient: chatClient,
            name: "TestAgent",
            instructions: "你是一个测试助手,回答要简洁"
        );
    }
    
    [Fact]
    public async Task Agent_ShouldRespondToSimpleQuestion()
    {
        // Arrange
        var thread = new AgentThread();
        await thread.AddUserMessageAsync("1+1等于几?");
        
        // Act
        var response = await _agent.InvokeAsync(thread);
        
        // Assert
        response.Should().NotBeNull();
        response.Content.Should().Contain("2");
    }
    
    [Fact]
    public async Task Agent_ShouldMaintainContext()
    {
        // Arrange
        var thread = new AgentThread();
        
        // 第一轮对话
        await thread.AddUserMessageAsync("我叫张三");
        var response1 = await _agent.InvokeAsync(thread);
        
        // 第二轮对话
        await thread.AddUserMessageAsync("我叫什么名字?");
        var response2 = await _agent.InvokeAsync(thread);
        
        // Assert
        response2.Content.Should().Contain("张三");
    }
    
    public void Dispose()
    {
        // 清理资源
    }
}

2. 测试工具调用

复制代码
// AgentApp/Tools/CalculatorTool.cs
public class CalculatorTool
{
    [Description("计算两个数的和")]
    public int Add(
        [Description("第一个数")] int a,
        [Description("第二个数")] int b)
    {
        return a + b;
    }
    
    [Description("计算两个数的乘积")]
    public int Multiply(
        [Description("第一个数")] int a,
        [Description("第二个数")] int b)
    {
        return a * b;
    }
}

集成测试

复制代码
// AgentApp.Tests/Integration/ToolIntegrationTests.cs
public class ToolIntegrationTests
{
    private readonly ChatCompletionAgent _agent;
    
    public ToolIntegrationTests()
    {
        var chatClient = CreateChatClient(); // 创建真实客户端
        var calculator = new CalculatorTool();
        
        _agent = new ChatCompletionAgent(
            chatClient: chatClient,
            name: "CalculatorAgent",
            instructions: "你是一个计算助手,使用工具进行计算"
        )
        {
            Tools = [AIFunctionFactory.Create(calculator.Add), 
                     AIFunctionFactory.Create(calculator.Multiply)]
        };
    }
    
    [Fact]
    public async Task Agent_ShouldUseAddTool()
    {
        // Arrange
        var thread = new AgentThread();
        await thread.AddUserMessageAsync("计算 15 + 27");
        
        // Act
        var response = await _agent.InvokeAsync(thread);
        
        // Assert
        response.Content.Should().Contain("42");
    }
    
    [Fact]
    public async Task Agent_ShouldUseMultiplyTool()
    {
        // Arrange
        var thread = new AgentThread();
        await thread.AddUserMessageAsync("计算 6 乘以 7");
        
        // Act
        var response = await _agent.InvokeAsync(thread);
        
        // Assert
        response.Content.Should().Contain("42");
    }
    
    private IChatClient CreateChatClient()
    {
        // 实际实现
        return null;
    }
}

3. 测试多轮对话

复制代码
public class ConversationIntegrationTests
{
    [Fact]
    public async Task MultiTurnConversation_ShouldMaintainContext()
    {
        // Arrange
        var agent = CreateAgent();
        var thread = new AgentThread();
        
        // 第一轮:设置上下文
        await thread.AddUserMessageAsync("我想买一台笔记本电脑");
        var response1 = await agent.InvokeAsync(thread);
        response1.Content.Should().NotBeNullOrEmpty();
        
        // 第二轮:基于上下文提问
        await thread.AddUserMessageAsync("预算在 5000 元左右");
        var response2 = await agent.InvokeAsync(thread);
        response2.Content.Should().Contain("笔记本");
        
        // 第三轮:继续对话
        await thread.AddUserMessageAsync("有什么推荐吗?");
        var response3 = await agent.InvokeAsync(thread);
        response3.Content.Should().NotBeNullOrEmpty();
    }
    
    private ChatCompletionAgent CreateAgent()
    {
        // 实际实现
        return null;
    }
}

测试最佳实践

1. 测试命名规范

使用清晰的命名让测试易于理解:

复制代码
// 格式:MethodName_Scenario_ExpectedBehavior

[Fact]
public void MaskPhoneNumber_ValidPhone_ShouldMaskMiddleDigits() { }

[Fact]
public void ValidateInput_EmptyMessage_ShouldReturnFalse() { }

[Fact]
public void ProcessMessage_WithRetry_ShouldSucceedAfterFailure() { }

2. AAA 模式

每个测试分为三个部分:

复制代码
[Fact]
public void Example_Test()
{
    // Arrange(准备):设置测试数据和依赖
    var input = "test input";
    var expected = "expected output";
    
    // Act(执行):调用被测试的方法
    var result = MethodUnderTest(input);
    
    // Assert(断言):验证结果
    result.Should().Be(expected);
}

3. 一个测试只验证一件事

复制代码
// ❌ 不好:一个测试验证多件事
[Fact]
public void ProcessMessage_ShouldValidateAndCallApiAndReturnResult()
{
    // 测试太多东西
}

// ✅ 好:分成多个测试
[Fact]
public void ProcessMessage_InvalidInput_ShouldThrowException() { }

[Fact]
public void ProcessMessage_ValidInput_ShouldCallApi() { }

[Fact]
public void ProcessMessage_ApiSuccess_ShouldReturnResult() { }

4. 使用测试数据生成器

复制代码
public class TestDataBuilder
{
    public static string CreateValidMessage(int length = 100)
    {
        return new string('a', length);
    }
    
    public static string CreateInvalidMessage()
    {
        return new string('a', 5000); // 超过限制
    }
    
    public static AgentThread CreateThreadWithHistory(int messageCount)
    {
        var thread = new AgentThread();
        for (int i = 0; i < messageCount; i++)
        {
            thread.AddUserMessageAsync($"消息 {i}").Wait();
        }
        return thread;
    }
}

// 使用
[Fact]
public void Test_WithGeneratedData()
{
    var message = TestDataBuilder.CreateValidMessage(50);
    // 使用 message 进行测试
}

5. 测试异步代码

复制代码
[Fact]
public async Task AsyncMethod_ShouldReturnExpectedResult()
{
    // Arrange
    var service = new AgentService();
    
    // Act
    var result = await service.ProcessMessageAsync("test");
    
    // Assert
    result.Should().NotBeNull();
}

[Fact]
public async Task AsyncMethod_ShouldThrowException()
{
    // Arrange
    var service = new AgentService();
    
    // Act & Assert
    await Assert.ThrowsAsync<ArgumentException>(
        async () => await service.ProcessMessageAsync("")
    );
}

测试覆盖率

1. 安装覆盖率工具

复制代码
dotnet add package coverlet.collector

2. 运行覆盖率测试

复制代码
dotnet test --collect:"XPlat Code Coverage"

3. 生成覆盖率报告

复制代码
# 安装报告生成工具
dotnet tool install -g dotnet-reportgenerator-globaltool

# 生成报告
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coveragereport" -reporttypes:Html

# 查看报告
start coveragereport/index.html

4. 覆盖率目标

  • 核心业务逻辑:80% 以上

  • 工具函数:90% 以上

  • UI 代码:50% 以上(可选)

持续集成中的测试

1. GitHub Actions 配置

复制代码
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: '8.0.x'
    
    - name: Restore dependencies
      run: dotnet restore
    
    - name: Build
      run: dotnet build --no-restore
    
    - name: Run tests
      run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"
    
    - name: Upload coverage
      uses: codecov/codecov-action@v2

测试检查清单

在提交代码之前,使用这个清单检查测试:

  • \] **单元测试** * \[ \] 所有工具函数都有测试 * \[ \] 所有验证逻辑都有测试 * \[ \] 边界条件都有测试 * \[ \] 异常情况都有测试

    • \] AI 服务调用有测试

    • \] 多轮对话有测试

    • \] 测试命名清晰

    • \] 一个测试只验证一件事

  • \] **覆盖率** * \[ \] 核心逻辑覆盖率 \> 80% * \[ \] 关键路径都有测试

    • \] 配置了自动测试

小结

测试是保证代码质量的重要手段,关键要点:

  1. 单元测试:测试单个函数,使用 Mock 隔离依赖

  2. 集成测试:测试组件协作,验证真实场景

  3. 测试命名:清晰描述测试内容

  4. AAA 模式:准备、执行、断言

  5. 测试覆盖率:核心逻辑要有足够覆盖

  6. 持续集成:自动运行测试

记住:好的测试不仅能发现 bug,还能作为代码的文档

更多AIGC文章

RAG技术全解:从原理到实战的简明指南

更多VibeCoding文章

相关推荐
墨痕诉清风2 天前
java漏洞集合工具(Struts2、Fastjson、Weblogic(xml)、Shiro、Log4j、Jboss、SpringCloud)
xml·java·struts·安全·web安全·spring cloud·log4j
Lisonseekpan2 天前
为什么Spring 推荐使用构造器注入而非@Autowired字段注入?
java·后端·spring·log4j
brave and determined3 天前
CANN训练营 学习(day10)昇腾AI算子ST测试全攻略:从入门到精通
自动化测试·人工智能·log4j·算子·fuzz·测试实战·st测试
記億揺晃着的那天4 天前
MyBatis-Plus 单元测试中 Lambda Mock 的坑与解决
单元测试·log4j·mybatis
m0_740043735 天前
SpringBoot05-配置文件-热加载/日志框架slf4j/接口文档工具Swagger/Knife4j
java·spring boot·后端·log4j
她说彩礼65万6 天前
C# params使用
开发语言·c#·log4j
掘根9 天前
【消息队列项目】公共模块实现
log4j
代码欢乐豆11 天前
软件测试测试题——单元测试
软件测试·log4j