- 新增 GatewayAggregate 领域实体 (GwTenant, GwTenantRoute, GwServiceInstance) - 新增 IRouteStore, RouteStore, IInstanceStore, InstanceStore - 新增 IRouteManager, RouteManager - 合并 GatewayDbContext 到 PlatformDbContext - 统一 Extensions.AddPlatformCore 注册所有服务
248 lines
5.7 KiB
Markdown
248 lines
5.7 KiB
Markdown
# 测试模式
|
||
|
||
**分析日期:** 2026-02-28
|
||
|
||
## 测试框架
|
||
|
||
**状态:** 此仓库中目前不存在测试项目。
|
||
|
||
### 预期框架(未实现)
|
||
|
||
基于项目结构和依赖,测试项目应使用:
|
||
|
||
- **测试运行器:** xUnit(.NET 标准)
|
||
- **模拟:** Moq 或 NSubstitute
|
||
- **内存数据库:** `Microsoft.EntityFrameworkCore.InMemory` 用于 DbContext 测试
|
||
- **断言:** FluentAssertions(可选,用于可读断言)
|
||
|
||
### 建议的项目结构
|
||
|
||
```
|
||
Fengling.Platform.Tests/
|
||
├── Fengling.Platform.Tests.csproj
|
||
├── Unit/
|
||
│ ├── TenantManagerTests.cs
|
||
│ ├── TenantStoreTests.cs
|
||
│ └── PlatformDbContextTests.cs
|
||
├── Integration/
|
||
│ └── TenantRepositoryTests.cs
|
||
└── Usings.cs
|
||
```
|
||
|
||
## 测试文件组织
|
||
|
||
### 位置
|
||
- **模式:** 单独测试项目(`Fengling.Platform.Tests/`)
|
||
- **结构:** 镜像源项目结构
|
||
|
||
### 命名
|
||
- **测试类:** `{类名}Tests` 或 `{类名}Tests`
|
||
- **测试方法:** `{方法名}_{场景}_{预期结果}`
|
||
|
||
## 测试结构
|
||
|
||
### 套件组织(预期模式)
|
||
|
||
```csharp
|
||
public class TenantManagerTests
|
||
{
|
||
private readonly ITenantManager _tenantManager;
|
||
private readonly Mock<ITenantStore> _storeMock;
|
||
|
||
public TenantManagerTests()
|
||
{
|
||
_storeMock = new Mock<ITenantStore>();
|
||
_tenantManager = new TenantManager(_storeMock.Object);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task FindByIdAsync_WithValidId_ReturnsTenant()
|
||
{
|
||
// Arrange
|
||
var tenantId = 1L;
|
||
var expectedTenant = new Tenant { Id = tenantId, Name = "Test" };
|
||
_storeMock.Setup(s => s.FindByIdAsync(tenantId, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(expectedTenant);
|
||
|
||
// Act
|
||
var result = await _tenantManager.FindByIdAsync(tenantId);
|
||
|
||
// Assert
|
||
Assert.NotNull(result);
|
||
Assert.Equal(tenantId, result.Id);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 模拟
|
||
|
||
### 框架: Moq
|
||
|
||
**要模拟:**
|
||
- `ITenantStore` - `TenantManager` 的依赖
|
||
- `PlatformDbContext` - 用于仓储测试
|
||
|
||
### 模式
|
||
|
||
```csharp
|
||
// 带参数的模拟设置
|
||
_storeMock.Setup(s => s.FindByIdAsync(tenantId, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(expectedTenant);
|
||
|
||
// 空返回模拟
|
||
_storeMock.Setup(s => s.FindByIdAsync(It.IsAny<long?>(), It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync((Tenant?)null);
|
||
|
||
// 验证调用
|
||
_storeMock.Verify(s => s.CreateAsync(It.IsAny<Tenant>(), It.IsAny<CancellationToken>()), Times.Once);
|
||
```
|
||
|
||
**不要模拟:**
|
||
- `Tenant` 实体(使用真实实例)
|
||
- 值类型和简单 DTO
|
||
|
||
## 测试夹具和工厂
|
||
|
||
### 测试数据
|
||
|
||
```csharp
|
||
public static class TenantFixture
|
||
{
|
||
public static Tenant CreateValidTenant(long id = 1)
|
||
=> new Tenant
|
||
{
|
||
Id = id,
|
||
TenantCode = "TEST",
|
||
Name = "Test Tenant",
|
||
ContactName = "John Doe",
|
||
ContactEmail = "john@test.com",
|
||
Status = TenantStatus.Active,
|
||
CreatedAt = DateTime.UtcNow
|
||
};
|
||
|
||
public static IEnumerable<Tenant> CreateTenantCollection(int count)
|
||
=> Enumerable.Range(1, count)
|
||
.Select(i => CreateValidTenant(i));
|
||
}
|
||
```
|
||
|
||
### 位置
|
||
- `Tests/Fixtures/` 或 `Tests/Factories/`
|
||
|
||
## 测试类型
|
||
|
||
### 单元测试
|
||
- **范围:** 隔离的单个类
|
||
- 目标: `TenantManager` - CRUD 操作
|
||
- 目标: `TenantStore` - 数据访问方法
|
||
- 目标: 实体验证逻辑
|
||
- **方法:** 模拟依赖,隔离测试
|
||
|
||
### 集成测试
|
||
- **范围:** 使用 `PlatformDbContext` 的数据库操作
|
||
- **方法:**
|
||
- 使用 `InMemoryDatabase` 隔离
|
||
- 使用 `DbContextOptionsBuilder` + `UseInMemoryDatabase`
|
||
|
||
```csharp
|
||
public class PlatformDbContextTests
|
||
{
|
||
private readonly PlatformDbContext _context;
|
||
|
||
public PlatformDbContextTests()
|
||
{
|
||
var options = new DbContextOptionsBuilder<PlatformDbContext>()
|
||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||
.Options;
|
||
_context = new PlatformDbContext(options);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 端到端测试
|
||
- **不适用:** 这是一个库/项目,不是应用程序
|
||
|
||
## 常见模式
|
||
|
||
### 异步测试
|
||
|
||
```csharp
|
||
[Fact]
|
||
public async Task GetAllAsync_ReturnsAllTenants()
|
||
{
|
||
// Arrange
|
||
var tenants = new List<Tenant> { CreateValidTenant(), CreateValidTenant(2) };
|
||
_storeMock.Setup(s => s.GetAllAsync(It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(tenants);
|
||
|
||
// Act
|
||
var result = await _tenantManager.GetAllAsync();
|
||
|
||
// Assert
|
||
Assert.Equal(2, result.Count);
|
||
}
|
||
```
|
||
|
||
### 错误测试
|
||
|
||
```csharp
|
||
[Fact]
|
||
public async Task CreateAsync_WithDuplicateTenantCode_ReturnsFailure()
|
||
{
|
||
// Arrange
|
||
var tenant = CreateValidTenant();
|
||
_storeMock.Setup(s => s.FindByTenantCodeAsync(tenant.TenantCode, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(tenant); // 模拟已存在的租户
|
||
|
||
// Act
|
||
var result = await _tenantManager.CreateAsync(tenant);
|
||
|
||
// Assert
|
||
Assert.False(result.Succeeded);
|
||
}
|
||
```
|
||
|
||
### 空参数测试
|
||
|
||
```csharp
|
||
[Fact]
|
||
public async Task FindByIdAsync_WithNullId_ReturnsNull()
|
||
{
|
||
// Act
|
||
var result = await _tenantManager.FindByIdAsync(null);
|
||
|
||
// Assert
|
||
Assert.Null(result);
|
||
}
|
||
```
|
||
|
||
## 覆盖率
|
||
|
||
### 要求
|
||
- **未强制** - 目前未定义覆盖率目标
|
||
|
||
### 建议目标
|
||
- **最低:** 领域逻辑 70%
|
||
- **目标:** 关键路径(租户 CRUD)80%
|
||
|
||
### 查看覆盖率
|
||
```bash
|
||
dotnet test --collect:"XPlat Code Coverage"
|
||
# 或使用 coverlet
|
||
dotnet test /p:CollectCoverage=true /p:Threshold=80
|
||
```
|
||
|
||
## 运行测试
|
||
|
||
### 命令(预期)
|
||
```bash
|
||
dotnet test # 运行所有测试
|
||
dotnet test --filter "FullyQualifiedName~TenantManagerTests" # 运行特定类
|
||
dotnet test --verbosity normal # 详细输出
|
||
dotnet test --collect:"XPlat Code Coverage" # 带覆盖率
|
||
```
|
||
|
||
---
|
||
|
||
*测试分析: 2026-02-28*
|