SeatFlow 测试架构(4 个 xUnit v3 项目)、Headless UI 视觉回归、FluentAssertions/NSubstitute 使用规范和测试编写指南
本页目录
全部文档
测试指南
测试技术栈
| 组件 | 版本 | 用途 |
|---|---|---|
| xUnit v3 | 4.0.1(3 个核心层项目);3.2.2(Presentation.Tests 锁定) | 测试框架 |
| FluentAssertions | 8.11.0 | 可读性断言 |
| NSubstitute | 6.2.0 | Mock/Stub 对象 |
| Microsoft.Testing.Platform | — | 测试运行器(global.json 指定 MTP) |
| Avalonia.Headless.XUnit | 12.1.3 | Headless UI 测试(Presentation.Tests) |
| Avalonia.Skia | 12.1.3 | 真实绘制与视觉基线(Presentation.Tests) |
版本说明
xUnit v3 通过根 global.json 的 "test": { "runner": "Microsoft.Testing.Platform" } 指定 Microsoft.Testing.Platform 作为测试运行器。运行时输出和 CLI 行为与 v2 有所不同。
Headless UI 测试项目有两条必须遵守的版本锁定:
- xunit.v3 3.2.2:
Avalonia.Headless.XUnit 12.1.3按xunit.v3.extensibility.core 3.2.2编译,若使用 4.x 会抛MissingMethodException。其余 3 个测试项目当前使用 4.0.1,各.csproj独立管理版本(仓库没有Directory.Packages.props)。 SkiaSharp.NativeAssets.Linux(4.153.1):显式引用以与托管SkiaSharp对齐;依赖链默认解析到旧版 Linux 原生资产时,SkiaSharpVersion.CheckNativeLibraryCompatible会在 Linux 上崩溃。
测试项目结构
4 个测试项目位于 tests/ 目录,其中 3 个对应核心层,另 1 个为 Headless UI 测试:
SeatFlow.slnx
└── tests/
├── SeatFlow.Core.Tests/ 195 用例 → 领域实体、策略逻辑、领域服务
├── SeatFlow.Application.Tests/ 61 用例 → 外观模式、管道执行、命令历史
├── SeatFlow.Infrastructure.Tests/ 125 用例 → 数据提供者、导出器、迁移、仓库
└── SeatFlow.Presentation.Tests/ 67 用例 → Headless UI、视觉基线、生命周期契约
全量 dotnet test 共发现 448 个测试(0 失败为通过标准)。
项目配置
每个测试项目启用了 <ImplicitUsings>enable</ImplicitUsings>,自动引入:
<ImplicitUsings>enable</ImplicitUsings>
自动引入的命名空间:
SystemSystem.Collections.GenericSystem.LinqSystem.Threading.Tasks
项目特定的全局 using 在 Usings.cs(Application.Tests 中为 Using.cs,Presentation.Tests 中为 GlobalUsings.cs)中定义。
// Core.Tests/Usings.cs 示例
global using Xunit;
global using FluentAssertions;
global using NSubstitute;
global using SeatFlow.Core; // 项目特定引用
SeatFlow.Presentation.Tests 通过 TestAppBuilder.cs 装配 Headless 应用:
[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))]
public static class TestAppBuilder
{
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder
.Configure(() => new SeatFlow.Presentation.Avalonia.App(provider))
.UseSkia()
// false = 关闭假绘制,启用 Skia 真实绘制(视觉基线用)
.UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false })
.LogToTrace();
}
测试复用生产 App 实例(完整加载 App.axaml 的主题/令牌/样式资源),避免测试与生产资源漂移;只注入最小 DI(语言设置读取的 IApplicationFacade 替身等)。
运行测试
运行全部测试
dotnet test
运行单个测试
dotnet test --filter "FullyQualifiedName~TestName"
按类别运行
# 按测试类名过滤
dotnet test --filter "FullyQualifiedName~Migration"
# 只运行单个测试项目
dotnet test tests/SeatFlow.Core.Tests
编写测试
测试类结构
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] 的内联参数 |
[AvaloniaFact] |
Headless UI 测试(Presentation.Tests) |
[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");
Headless UI 测试(SeatFlow.Presentation.Tests)
运行在无头环境(不需要显示器),但使用 Avalonia.Skia 真实绘制,覆盖 VM/服务单测与像素级回归:
| 测试类 | 覆盖内容 |
|---|---|
SeatingCanvasTests |
画布几何/命中/缩放/键盘虚拟焦点 |
PageLifecycleTests |
IPageLifecycle(OnEnterAsync/OnLeaveAsync/InitializationTask)契约 |
CrossCuttingServicesTests |
DirtyTracker、IDialogGate、IBusyScope 等横切服务 |
GuideSeedTargetTests |
IGuideSeedTarget seed/clear 幂等性 |
SettingsViewModelTests / StudentRowViewModelTests |
页面 VM 行为 |
AccentButtonForegroundTests / SelectListColorTests |
颜色像素回归(按钮悬停前景、列表选中底色) |
VisualBaselineTests |
明/暗双主题 × 7 个关键视图的渲染基线 |
SmokeTests |
Headless 应用可创建窗口并渲染、令牌资源可解析 |
视觉基线
VisualBaselineTests 以 window.CaptureRenderedFrame() 渲染 7 个关键视图(工作台空态/生成态、排座画布、人员名单、设置、关于、座位长名换行)在明/暗双主题下的基线,输出 PNG 到 scripts/ui-inspect/baselines/(基线 PNG 不入库,目录已 gitignore;可用 SEATFLOW_BASELINE_DIR 环境变量覆盖输出目录)。测试本身只断言可渲染性(帧非空、文件可写),人工像素对比流程见 docs/UI_REFACTOR.md 的「验证与工具链」。
测试项目结构约定
tests/SeatFlow.Core.Tests/
├── Usings.cs # 全局 using
├── Strategies/
│ ├── FixedSeatStrategyTests.cs
│ ├── RandomFillStrategyTests.cs
│ └── DeskMateStrategyTests.cs
├── DomainServices/
│ ├── SeatGeometryHelperTests.cs
│ └── ObstacleProcessorTests.cs
└── Workspace/
└── SeatingWorkspaceTests.cs
tests/SeatFlow.Application.Tests/
├── Using.cs # 全局 using
├── Facade/
│ └── ApplicationFacadeTests.cs
└── Pipeline/
├── StrategyExecutionPipelineTests.cs
└── CommandHistoryTests.cs
tests/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
tests/SeatFlow.Presentation.Tests/
├── GlobalUsings.cs # 全局 using
├── TestAppBuilder.cs # AvaloniaTestApplication + Skia 真实绘制
├── VisualBaselineTests.cs # 明暗基线捕获
└── ...(画布、生命周期、横切服务、页面 VM)
测试约定
不依赖外部资源
测试应完全在内存中执行,不依赖文件系统、数据库或网络。对于基础设施层的文件 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("中文");
}
脚本测试
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
# 仅运行发布编排脚本测试
python3 -m pytest tests/test_release.py -v
# 仅运行打包历史同步脚本测试
python3 -m pytest tests/test_sync_velopack_history.py -v
| 测试文件 | 测试数量 | 说明 |
|---|---|---|
tests/test_i18n.py |
45 | i18n 资源管理脚本测试 |
tests/test_version.py |
25 | 版本管理脚本测试 |
tests/test_release.py |
21 | 发布编排脚本测试(SHA256/尺寸格式化、配置与版本读取、发布说明生成、vpk 打包编排 dry-run 等) |
tests/test_sync_velopack_history.py |
20 | Velopack 打包历史同步脚本测试(保留集、双向对账、MD5/ETag、dry-run、main 退出码) |
tests/test_stage_velopack_artifacts.py |
4 | 发布产物暂存脚本测试(复制/排除集、稳定版白名单清理、channel 文件缺失失败) |
脚本测试内容
test_i18n.py:覆盖所有子命令(list, check, add, modify, rename, delete, sync, export, import)、安全机制(备份、dry-run、原子性)、XML 合法性验证、文件同步一致性test_version.py:覆盖版本号读取校验、一致性检查、自动同步、各种 bump 操作、安全机制test_release.py:覆盖发布编排的纯函数与流程分支(SHA256、文件分发过滤、版本化命名、RELEASE.md 组装、vpk 调用与 dry-run)test_sync_velopack_history.py:覆盖保留集计算(全部 delta + 最新 2 个 full)、双向对账(下载/删除/字节统计)、size 与 MD5/ETag 校验、dry-run 零副作用、main()退出码test_stage_velopack_artifacts.py:覆盖产物复制与排除(assets.*.json、旧 nupkg、Portable.zip)、稳定版白名单清理、channel 文件缺失失败
测试驱动开发
推荐在以下场景使用 TDD:
- 策略实现:先定义期望的分配行为,再编写策略逻辑
- 迁移器:先编写旧格式的 JSON 输入和期望的新格式输出
- 数据提供者:先定义数据解析结果,再实现解析逻辑
持续集成
每次提交前确保:
dotnet build # 编译通过(含双 TFM)
dotnet test # 全部 448 个用例通过
python3 -m pytest scripts/tests/ -v # 脚本测试通过
也可使用根 global.json 定义的本地任务(首次先 dotnet tool restore):
dotnet r build
dotnet r test
dotnet r ci # build + test
CI 现状:
unit-tests.yml目前只条件运行 Core / Infrastructure / Application 三个项目(Presentation.Tests 暂未纳入 CI),因此改动 UI 层后请在本地跑全量dotnet test。涉及浏览器端(net10.0-browser)的改动,CI 会安装 WASM 工作负载并验证浏览器壳可正常构建。
相关文档
- 主仓库
AGENTS.md(构建与测试章节) - 主仓库
CONTRIBUTING.md(环境搭建) - CI/CD 工作流