铜仁网站建设公司,成都设计公司推荐,济南市新闻最新消息,西安响应式网站开发概述
测试是确保 AI 代理应用质量的关键。本文将介绍单元测试和集成测试的方法#xff0c;并提供实用的测试代码示例。
为什么测试很重要#xff1f;
想象一下#xff0c;如果你的 AI 客服在上线后才发现无法正确回答用户问题#xff0c;或者工具调用失败#xff0c;会…概述测试是确保 AI 代理应用质量的关键。本文将介绍单元测试和集成测试的方法并提供实用的测试代码示例。为什么测试很重要想象一下如果你的 AI 客服在上线后才发现无法正确回答用户问题或者工具调用失败会造成多大的损失测试就像给你的应用做体检在问题发生之前就发现并修复它们。测试的好处提前发现问题在开发阶段就发现 bug保证质量确保功能按预期工作安全重构修改代码时不怕破坏现有功能文档作用测试代码展示了如何使用功能测试的类型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 FluentAssertions3. 测试工具函数假设我们有一个数据脱敏工具// 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 联系我zhangsanexample.com; // Act var result _masker.MaskEmail(input); // Assert result.Should().Be(联系我zh***example.com); } [Fact] public void MaskEmail_ShortUsername_ShouldMaskCompletely() { // Arrange var input 邮箱abtest.com; // Act var result _masker.MaskEmail(input); // Assert result.Should().Be(邮箱***test.com); } }运行测试dotnet test4. 测试输入验证// 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 { Taskstring 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 Taskstring 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 MockIChatClient _mockChatClient; private readonly InputValidator _validator; private readonly AgentService _service; public AgentServiceTests() { _mockChatClient new MockIChatClient(); _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.ThrowsAsyncArgumentException( async () await _service.ProcessMessageAsync(message) ); // 验证没有调用 ChatClient _mockChatClient.Verify(x x.SendMessageAsync(It.IsAnystring()), 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(11等于几); // 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.ThrowsAsyncArgumentException( async () await service.ProcessMessageAsync() ); }测试覆盖率1. 安装覆盖率工具dotnet add package coverlet.collector2. 运行覆盖率测试dotnet test --collect:XPlat Code Coverage3. 生成覆盖率报告# 安装报告生成工具 dotnet tool install -g dotnet-reportgenerator-globaltool # 生成报告 reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coveragereport -reporttypes:Html # 查看报告 start coveragereport/index.html4. 覆盖率目标核心业务逻辑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/checkoutv2 - name: Setup .NET uses: actions/setup-dotnetv1 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-actionv2测试检查清单在提交代码之前使用这个清单检查测试[ ]单元测试[ ] 所有工具函数都有测试[ ] 所有验证逻辑都有测试[ ] 边界条件都有测试[ ] 异常情况都有测试[ ]集成测试[ ] AI 服务调用有测试[ ] 工具调用有测试[ ] 多轮对话有测试[ ]测试质量[ ] 测试命名清晰[ ] 使用 AAA 模式[ ] 一个测试只验证一件事[ ] 测试是独立的不依赖其他测试[ ]覆盖率[ ] 核心逻辑覆盖率 80%[ ] 关键路径都有测试[ ]持续集成[ ] 配置了自动测试[ ] 测试失败会阻止合并小结测试是保证代码质量的重要手段关键要点单元测试测试单个函数使用 Mock 隔离依赖集成测试测试组件协作验证真实场景测试命名清晰描述测试内容AAA 模式准备、执行、断言测试覆盖率核心逻辑要有足够覆盖持续集成自动运行测试记住好的测试不仅能发现 bug还能作为代码的文档。更多AIGC文章RAG技术全解从原理到实战的简明指南更多VibeCoding文章