SeatFlow 测试架构、xUnit v3、FluentAssertions、NSubstitute 使用规范和测试编写指南
本页目录
全部文档
测试指南
测试技术栈
| 组件 | 版本 | 用途 |
|---|---|---|
| xUnit | v3 | 测试框架 |
| FluentAssertions | — | 可读性断言 |
| NSubstitute | — | Mock/Stub 对象 |
| Microsoft.Testing.Platform | — | 测试运行器 |
版本说明
xUnit v3 使用 Microsoft.Testing.Platform 作为测试运行器,而非传统的 xunit.runner.visualstudio。运行时输出和 CLI 行为与 v2 有所不同。
测试项目结构
3 个测试项目对应 3 个核心层:
SeatFlow.slnx
├── SeatFlow.Core.Tests/ → 领域实体、策略逻辑、领域服务
├── SeatFlow.Application.Tests/ → 外观模式、管道执行、命令历史
└── SeatFlow.Infrastructure.Tests/ → 数据提供者、导出器、迁移、仓库
项目配置
每个测试项目启用了 <ImplicitUsings>enable</ImplicitUsings>,自动引入:
<ImplicitUsings>enable</ImplicitUsings>
自动引入的命名空间:
SystemSystem.Collections.GenericSystem.LinqSystem.Threading.Tasks
项目特定的全局 using 在 Usings.cs(Application.Tests 中为 Using.cs)中定义。
// Core.Tests/Usings.cs 示例
global using Xunit;
global using FluentAssertions;
global using NSubstitute;
global using SeatFlow.Core; // 项目特定引用
运行测试
运行全部测试
dotnet test
运行单个测试
dotnet test --filter "FullyQualifiedName~TestName"
按类别运行
# 按测试类名过滤
dotnet test --filter "FullyQualifiedName~Migration"
# 按特性过滤(如果使用了 xUnit Traits)
dotnet test --filter "Category=Integration"
编写测试
测试类结构
public class FixedSeatStrategyTests
{
[Fact]
public void ExecuteAsync_WithValidFixedSeats_ShouldAssignStudents()
{
// Arrange
var workspace = new SeatingWorkspace(/* ... */);
var strategy = new FixedSeatStrategy();
// Act
await strategy.ExecuteAsync(workspace, CancellationToken.None);
// Assert
workspace.GetAssignedSeats()
.Should().NotBeEmpty()
.And.HaveCount(expectedCount);
}
}
测试命名约定
{ClassUnderTest}_{Scenario}_{ExpectedBehavior}
| 部分 | 说明 | 示例 |
|---|---|---|
| ClassUnderTest | 被测试的类/方法 | ExecuteAsync |
| Scenario | 测试场景描述 | WithValidFixedSeats |
| ExpectedBehavior | 期望行为 | ShouldAssignStudents |
三段式结构
每个测试方法遵循 Arrange-Act-Assert 模式,用空行分隔:
[Fact]
public void Method_Scenario_ExpectedResult()
{
// Arrange
var sut = new SystemUnderTest();
// Act
var result = sut.DoSomething();
// Assert
result.Should().Be(expected);
}
测试特性
| 特性 | 用途 |
|---|---|
[Fact] |
普通测试方法 |
[Theory] |
参数化测试 |
[InlineData] |
[Theory] 的内联参数 |
[Theory]
[InlineData(1, 2, 3)]
[InlineData(0, 0, 0)]
public void Add_WithVariousInputs_ShouldReturnCorrectSum(int a, int b, int expected)
{
var result = a + b;
result.Should().Be(expected);
}
使用 NSubstitute
[Fact]
public async Task GenerateSeating_WhenCalled_ShouldExecuteStrategies()
{
// Arrange
var mockStrategy = Substitute.For<ISeatingStrategy>();
mockStrategy.Id.Returns("Test");
mockStrategy.Priority.Returns(50);
var facade = new ApplicationFacade(/* ... */);
// Act
await facade.GenerateSeatingAsync(/* ... */);
// Assert
await mockStrategy.Received(1).ExecuteAsync(Arg.Any<SeatingWorkspace>(), Arg.Any<CancellationToken>());
}
使用 FluentAssertions
// 基本断言
result.Should().Be(42);
result.Should().NotBeNull();
// 集合断言
list.Should().HaveCount(5).And.ContainSingle(x => x.Name == "Alice");
// 异常断言
action.Should().Throw<InvalidOperationException>()
.WithMessage("*already assigned*");
// 布尔断言
isValid.Should().BeTrue();
// 字符串断言
name.Should().StartWith("A").And.EndWith("z");
测试项目结构约定
SeatFlow.Core.Tests/
├── Usings.cs # 全局 using
├── Strategies/
│ ├── FixedSeatStrategyTests.cs
│ ├── RandomFillStrategyTests.cs
│ └── DeskMateStrategyTests.cs
├── DomainServices/
│ ├── SeatGeometryHelperTests.cs
│ └─. ObstacleProcessorTests.cs
└── Workspace/
└── SeatingWorkspaceTests.cs
SeatFlow.Application.Tests/
├── Using.cs # 全局 using
├── Facade/
│ └── ApplicationFacadeTests.cs
├── Pipeline/
│ ├── StrategyExecutionPipelineTests.cs
│ └── CommandHistoryTests.cs
└── Plugin/
└── PluginManagerTests.cs
SeatFlow.Infrastructure.Tests/
├── Usings.cs # 全局 using
├── Migration/
│ ├── VenueMigratorsTests.cs
│ ├── SeatSetsMigratorsTests.cs
│ └── FileMigrationServiceTests.cs
├── Providers/
│ ├── CsvStudentProviderTests.cs
│ └─. XlsxStudentProviderTests.cs
├── Exporters/
│ ├── ExcelSeatingExporterTests.cs
│ └── CsvSeatingExporterTests.cs
└── Repositories/
└── JsonVenueRepositoryTests.cs
测试约定
不依赖外部资源
测试应完全在内存中执行,不依赖文件系统、数据库或网络。对于基础设施层的文件 I/O 测试,使用临时目录:
[Fact]
public async Task LoadVenues_WithValidFile_ShouldReturnVenues()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
File.WriteAllText(Path.Combine(tempDir, "test.venue.json"), validJson);
var repo = new JsonVenueRepository(tempDir);
// Act
var venues = await repo.LoadVenuesAsync();
// Assert
venues.Should().NotBeEmpty();
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
静态方法测试
工具类和领域服务中的静态方法直接测试:
[Fact]
public void Resolve_WithZhCN_ShouldReturnChineseValue()
{
var dict = new Dictionary<string, string>
{
["zh-CN"] = "中文",
["en-US"] = "English"
};
var result = LocalizeHelper.Resolve(dict);
result.Should().Be("中文");
}
能力声明测试
能力声明系统要求测试中显式注册能力:
[Fact]
public void TryMarkFixed_WithDeclaredCapability_ShouldSucceed()
{
var workspace = new SeatingWorkspace(/* ... */);
workspace.RegisterCapabilities(new[] { "MarkFixedSeat" });
var result = ((IFixedSeatCapability)workspace)
.TryMarkFixed(seatId, studentId, "test", "Test Strategy", out var error);
result.Should().BeTrue();
}
脚本测试
Python 脚本的测试在 scripts/tests/ 目录中:
cd scripts
# 运行所有脚本测试
python3 -m pytest tests/ -v
# 仅运行 i18n 脚本测试
python3 -m pytest tests/test_i18n.py -v
# 仅运行版本管理脚本测试
python3 -m pytest tests/test_version.py -v
| 测试文件 | 测试数量 | 说明 |
|---|---|---|
tests/test_i18n.py |
45 | i18n 资源管理脚本测试 |
tests/test_version.py |
26 | 版本管理脚本测试 |
脚本测试内容
test_i18n.py:覆盖所有子命令(list, check, add, modify, rename, delete, sync, export, import)、安全机制(备份、dry-run、原子性)、XML 合法性验证、文件同步一致性test_version.py:覆盖版本号读取校验、一致性检查、自动同步、各种 bump 操作、安全机制
测试驱动开发
推荐在以下场景使用 TDD:
- 策略实现:先定义期望的分配行为,再编写策略逻辑
- 迁移器:先编写旧格式的 JSON 输入和期望的新格式输出
- 数据提供者:先定义数据解析结果,再实现解析逻辑
持续集成
每次提交前确保:
dotnet build # 编译通过
dotnet test # 全部测试通过
python3 -m pytest scripts/tests/ -v # 脚本测试通过