370 lines
12 KiB
Markdown
370 lines
12 KiB
Markdown
# 测试方案设计
|
||
|
||
> 所属项目: free-code .NET 10 重写
|
||
> 文档类型: 测试设计
|
||
> 来源章节: DESIGN-NET10-PART3.md § 22
|
||
> 上级文档: [测试与构建总览](测试与构建.md)
|
||
|
||
---
|
||
|
||
## 概述
|
||
|
||
原始 TypeScript 项目没有系统化的测试覆盖。.NET 重写从零建立分层测试体系,目标是核心路径 80%+ 覆盖率。测试框架组合为 **xUnit 2 + NSubstitute 5 + FluentAssertions 7**,三个独立测试项目分别承载单元、集成和 E2E 三个层次。
|
||
|
||
---
|
||
|
||
## 22.1 测试项目结构
|
||
|
||
```
|
||
tests/
|
||
├── FreeCode.Tests.Unit/ # 120+ 单元测试
|
||
│ ├── Engine/
|
||
│ │ ├── QueryEngineTests.cs
|
||
│ │ └── SystemPromptBuilderTests.cs
|
||
│ ├── Tools/
|
||
│ │ ├── BashToolTests.cs
|
||
│ │ ├── FileEditToolTests.cs
|
||
│ │ ├── FileReadToolTests.cs
|
||
│ │ ├── FileWriteToolTests.cs
|
||
│ │ ├── GlobToolTests.cs
|
||
│ │ ├── GrepToolTests.cs
|
||
│ │ ├── AgentToolTests.cs
|
||
│ │ └── ToolRegistryTests.cs
|
||
│ ├── Commands/
|
||
│ │ └── CommandRegistryTests.cs
|
||
│ ├── ApiProviders/
|
||
│ │ ├── AnthropicProviderTests.cs
|
||
│ │ ├── CodexProviderTests.cs
|
||
│ │ └── ProviderRouterTests.cs
|
||
│ ├── Mcp/
|
||
│ │ ├── McpClientTests.cs
|
||
│ │ ├── McpClientManagerTests.cs
|
||
│ │ └── TransportTests.cs
|
||
│ ├── Lsp/
|
||
│ │ └── LspClientManagerTests.cs
|
||
│ ├── Services/
|
||
│ │ ├── AuthServiceTests.cs
|
||
│ │ ├── RateLimitServiceTests.cs
|
||
│ │ ├── SessionMemoryServiceTests.cs
|
||
│ │ ├── CompanionServiceTests.cs
|
||
│ │ └── NotificationServiceTests.cs
|
||
│ └── State/
|
||
│ └── AppStateStoreTests.cs
|
||
├── FreeCode.Tests.Integration/ # 60 集成测试
|
||
│ ├── QueryPipelineTests.cs
|
||
│ ├── McpIntegrationTests.cs
|
||
│ ├── LspIntegrationTests.cs
|
||
│ ├── BridgeIntegrationTests.cs
|
||
│ ├── TaskManagerTests.cs
|
||
│ └── PluginLoadingTests.cs
|
||
└── FreeCode.Tests.E2E/ # 20 端到端测试
|
||
├── CliTests.cs
|
||
├── OneShotModeTests.cs
|
||
└── InteractiveReplTests.cs
|
||
```
|
||
|
||
---
|
||
|
||
## 22.2 单元测试示例
|
||
|
||
### BashToolTests.cs
|
||
|
||
测试覆盖: 命令执行、超时处理、后台任务、只读分类。
|
||
原始来源: `../../src/tools/BashTool.tsx`
|
||
|
||
```csharp
|
||
// === BashToolTests.cs ===
|
||
public class BashToolTests
|
||
{
|
||
private readonly BashTool _tool = new(
|
||
Substitute.For<IBackgroundTaskManager>(),
|
||
Substitute.For<IFeatureFlagService>());
|
||
|
||
[Fact]
|
||
public async Task ExecuteAsync_SimpleCommand_ReturnsOutput()
|
||
{
|
||
// Arrange
|
||
var input = new BashToolInput { Command = "echo hello" };
|
||
var context = new ToolExecutionContext(
|
||
WorkingDirectory: Path.GetTempPath(),
|
||
PermissionMode: PermissionMode.Default,
|
||
AdditionalWorkingDirectories: [],
|
||
PermissionEngine: Substitute.For<IPermissionEngine>(),
|
||
LspManager: Substitute.For<ILspClientManager>(),
|
||
TaskManager: Substitute.For<IBackgroundTaskManager>(),
|
||
Services: Substitute.For<IServiceProvider>());
|
||
|
||
// Act
|
||
var result = await _tool.ExecuteAsync(input, context);
|
||
|
||
// Assert
|
||
result.IsError.Should().BeFalse();
|
||
result.Data.Stdout.Trim().Should().Be("hello");
|
||
result.Data.ExitCode.Should().Be(0);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task ExecuteAsync_Timeout_KillsProcess()
|
||
{
|
||
var input = new BashToolInput { Command = "sleep 10", Timeout = 500 };
|
||
var context = CreateContext();
|
||
|
||
var result = await _tool.ExecuteAsync(input, context);
|
||
|
||
result.Data.Interrupted.Should().BeTrue();
|
||
result.Data.ExitCode.Should().Be(-1);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task ExecuteAsync_Background_ReturnsTaskId()
|
||
{
|
||
var taskManager = Substitute.For<IBackgroundTaskManager>();
|
||
taskManager.CreateShellTaskAsync(Arg.Any<string>(), Arg.Any<ProcessStartInfo>())
|
||
.Returns(new LocalShellTask { TaskId = "bg-123", Command = "sleep 10" });
|
||
|
||
var tool = new BashTool(taskManager, Substitute.For<IFeatureFlagService>());
|
||
var input = new BashToolInput { Command = "sleep 10", RunInBackground = true };
|
||
var result = await tool.ExecuteAsync(input, CreateContext());
|
||
|
||
result.Data.BackgroundTaskId.Should().Be("bg-123");
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData("ls", true)]
|
||
[InlineData("rm -rf /", false)]
|
||
[InlineData("cat file.txt", true)]
|
||
[InlineData("echo test > file", false)]
|
||
public void IsReadOnly_ClassifiesCorrectly(string command, bool expected)
|
||
{
|
||
var input = new BashToolInput { Command = command };
|
||
_tool.IsReadOnly(input).Should().Be(expected);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### ToolRegistryTests.cs
|
||
|
||
测试覆盖: 基础工具注册、MCP 工具去重策略。
|
||
原始来源: `../../src/tools.ts`
|
||
|
||
```csharp
|
||
// === ToolRegistryTests.cs ===
|
||
public class ToolRegistryTests
|
||
{
|
||
[Fact]
|
||
public async Task GetToolsAsync_ReturnsBaseTools()
|
||
{
|
||
var registry = CreateRegistry();
|
||
var tools = await registry.GetToolsAsync();
|
||
|
||
tools.Should().Contain(t => t.Name == "Read");
|
||
tools.Should().Contain(t => t.Name == "Edit");
|
||
tools.Should().Contain(t => t.Name == "Bash");
|
||
tools.Should().Contain(t => t.Name == "Agent");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task AssembleToolPool_DeduplicatesMcpTools()
|
||
{
|
||
// 内置 "Bash" 工具 + MCP 也提供 "Bash" → 只保留内置
|
||
var registry = CreateRegistry();
|
||
var mcpManager = Substitute.For<IMcpClientManager>();
|
||
mcpManager.GetToolsAsync().Returns(new List<ITool>
|
||
{
|
||
CreateTool("Bash"), // 与内置重名,应被丢弃
|
||
CreateTool("SlackSearch"), // 新工具,应被添加
|
||
});
|
||
|
||
// 验证: Bash 只有一个 (内置), SlackSearch 被添加
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### AnthropicProviderTests.cs
|
||
|
||
测试覆盖: SSE 流式事件解析。
|
||
原始来源: `../../src/services/api/claude.ts`
|
||
|
||
```csharp
|
||
// === AnthropicProviderTests.cs ===
|
||
public class AnthropicProviderTests
|
||
{
|
||
[Fact]
|
||
public async Task StreamAsync_ParsesSSECorrectly()
|
||
{
|
||
var sseResponse = "event: message_start\ndata: {\"type\":\"message_start\"}\n\n"
|
||
+ "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"text\":\"Hello\"}}\n\n"
|
||
+ "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
|
||
|
||
var handler = new MockHttpHandler(sseResponse);
|
||
var provider = new AnthropicProvider(new HttpClient(handler));
|
||
|
||
var messages = new List<SDKMessage>();
|
||
await foreach (var msg in provider.StreamAsync(new ApiRequest()))
|
||
messages.Add(msg);
|
||
|
||
messages.Should().ContainSingle(m => m is SDKMessage.StreamingDelta sd && sd.Text == "Hello");
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### CompanionServiceTests.cs
|
||
|
||
测试覆盖: 骰子结果确定性、不同用户产生不同结果。
|
||
原始来源: 无直接对应(新增 Companion 功能)
|
||
|
||
```csharp
|
||
// === CompanionServiceTests.cs ===
|
||
public class CompanionServiceTests
|
||
{
|
||
[Fact]
|
||
public void RollBones_IsDeterministic()
|
||
{
|
||
// 相同 userId → 相同骨骼
|
||
var service = new CompanionService(Substitute.For<IGlobalConfig>());
|
||
var c1 = service.RollBones("user-123");
|
||
var c2 = service.RollBones("user-123");
|
||
c1.Should().BeEquivalentTo(c2);
|
||
}
|
||
|
||
[Fact]
|
||
public void RollBones_DifferentUsers_DifferentResults()
|
||
{
|
||
var service = new CompanionService(Substitute.For<IGlobalConfig>());
|
||
var c1 = service.RollBones("user-A");
|
||
var c2 = service.RollBones("user-B");
|
||
// 极大概率不同 (18 种族, 6 眼形, 5 稀有度)
|
||
(c1.Species != c2.Species || c1.Eye != c2.Eye).Should().BeTrue();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 22.3 集成测试示例
|
||
|
||
### McpIntegrationTests.cs
|
||
|
||
测试覆盖: stdio MCP 服务器连接建立。
|
||
原始来源: `../../src/services/mcp/`
|
||
|
||
```csharp
|
||
// === McpIntegrationTests.cs ===
|
||
public class McpIntegrationTests : IAsyncLifetime
|
||
{
|
||
private McpClientManager _manager = null!;
|
||
private string _tempDir = null!;
|
||
|
||
public async Task InitializeAsync()
|
||
{
|
||
_tempDir = Path.Combine(Path.GetTempPath(), $"mcp-test-{Guid.NewGuid():N}");
|
||
Directory.CreateDirectory(_tempDir);
|
||
|
||
// 创建一个测试用 MCP stdio 服务器脚本
|
||
var serverScript = Path.Combine(_tempDir, "test-server.sh");
|
||
await File.WriteAllTextAsync(serverScript, @"#!/bin/bash
|
||
echo '{""jsonrpc"":""2.0"",""id"":1,""result"":{""protocolVersion"":""2025-03-26"",""capabilities"":{""tools"":{}}}}'
|
||
");
|
||
Process.Start("chmod", $"+x {serverScript}")?.WaitForExit();
|
||
}
|
||
|
||
[Fact]
|
||
public async Task ConnectServersAsync_ConnectsToStdioServer()
|
||
{
|
||
// 配置一个 stdio MCP 服务器
|
||
var config = new StdioServerConfig { Command = "echo", Scope = ConfigScope.Local };
|
||
// ... 验证连接成功
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 22.4 E2E 测试示例
|
||
|
||
### CliTests.cs
|
||
|
||
测试覆盖: 版本标志输出、一次性模式响应。
|
||
原始来源: `../../src/entrypoints/cli.tsx`
|
||
|
||
```csharp
|
||
// === CliTests.cs ===
|
||
public class CliTests
|
||
{
|
||
[Fact]
|
||
public async Task VersionFlag_PrintsVersion()
|
||
{
|
||
var result = await RunCliAsync("--version");
|
||
result.ExitCode.Should().Be(0);
|
||
result.Output.Should().StartWith("free-code ");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task OneShotMode_ReturnsResponse()
|
||
{
|
||
// 需要 mock API server
|
||
var result = await RunCliAsync("-p", "what is 2+2?");
|
||
result.ExitCode.Should().Be(0);
|
||
}
|
||
|
||
private static async Task<(int ExitCode, string Output)> RunCliAsync(params string[] args)
|
||
{
|
||
using var process = new Process
|
||
{
|
||
StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = "dotnet",
|
||
Arguments = $"run --project src/FreeCode {string.Join(" ", args)}",
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
CreateNoWindow = true,
|
||
}
|
||
};
|
||
process.Start();
|
||
var output = await process.StandardOutput.ReadToEndAsync();
|
||
await process.WaitForExitAsync();
|
||
return (process.ExitCode, output);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 22.5 测试金字塔
|
||
|
||
| 层次 | 项目 | 数量 | 执行时间目标 |
|
||
|------|------|------|------------|
|
||
| 单元测试 | `FreeCode.Tests.Unit` | 120+ | < 30 秒 |
|
||
| 集成测试 | `FreeCode.Tests.Integration` | 60 | < 3 分钟 |
|
||
| E2E 测试 | `FreeCode.Tests.E2E` | 20 | < 10 分钟 |
|
||
| **合计** | | **200** | |
|
||
|
||
单元测试覆盖所有工具、引擎、命令、API 提供商、MCP/LSP 客户端、服务层和状态管理。集成测试验证跨组件交互(查询管道、MCP 连接、LSP 通信、Bridge 握手、任务管理器、插件加载)。E2E 测试启动完整进程,验证 CLI 行为对外部用户可见的部分。
|
||
|
||
---
|
||
|
||
## 22.6 测试框架版本
|
||
|
||
| 包 | 版本 | 用途 |
|
||
|----|------|------|
|
||
| `xunit` | 2.x | 测试运行器和断言基础 |
|
||
| `NSubstitute` | 5.x | 接口 mock / stub / spy |
|
||
| `FluentAssertions` | 7.x | 可读的断言链 |
|
||
| `Microsoft.NET.Test.Sdk` | 最新 | dotnet test 集成 |
|
||
| `coverlet.collector` | 最新 | 代码覆盖率收集 |
|
||
|
||
---
|
||
|
||
## 参考资料
|
||
|
||
- [测试与构建总览](测试与构建.md)
|
||
- [原始代码映射 — 测试与构建](reference/原始代码映射-测试与构建.md)
|
||
- [核心模块设计 — 工具系统](../核心模块设计/核心模块设计-工具系统.md)
|
||
- [基础设施设计](../基础设施设计/基础设施设计.md)
|