跳转到主内容

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>

自动引入的命名空间:

  • System
  • System.Collections.Generic
  • System.Linq
  • System.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  # 脚本测试通过

相关文档