# YARP Gateway 测试文档
## 概述
本文档记录了 YARP Gateway 项目的测试策略、测试模式和最佳实践。
---
## 1. 测试框架
### 1.1 当前测试状态
**项目当前没有专门的测试目录或测试项目。**
检查项目结构:
```
fengling-gateway/
├── src/ # 源代码
│ └── YarpGateway.csproj # 主项目
├── .planning/
└── (无 tests/ 或 test/ 目录)
```
检查 `.csproj` 文件确认无测试框架依赖:
```xml
```
**结论**:项目目前处于开发阶段,尚未建立测试基础设施。
### 1.2 推荐测试框架
基于项目技术栈,推荐以下测试框架:
| 框架 | 用途 | NuGet 包 |
|------|------|----------|
| xUnit | 单元测试框架 | `xunit` |
| Moq | Mock 框架 | `Moq` |
| FluentAssertions | 断言库 | `FluentAssertions` |
| Microsoft.NET.Test.Sdk | 测试 SDK | `Microsoft.NET.Test.Sdk` |
| Testcontainers | 集成测试容器 | `Testcontainers.PostgreSql`, `Testcontainers.Redis` |
---
## 2. 推荐测试结构
### 2.1 测试项目组织
建议创建独立的测试项目:
```
tests/
├── YarpGateway.UnitTests/ # 单元测试
│ ├── Services/
│ │ ├── RouteCacheTests.cs
│ │ └── RedisConnectionManagerTests.cs
│ ├── Middleware/
│ │ ├── JwtTransformMiddlewareTests.cs
│ │ └── TenantRoutingMiddlewareTests.cs
│ └── Controllers/
│ └── GatewayConfigControllerTests.cs
│
├── YarpGateway.IntegrationTests/ # 集成测试
│ ├── GatewayEndpointsTests.cs
│ └── DatabaseTests.cs
│
└── YarpGateway.LoadTests/ # 负载测试(可选)
└── RoutePerformanceTests.cs
```
### 2.2 测试命名约定
```csharp
// 命名格式:[被测类]Tests
public class RouteCacheTests { }
// 方法命名格式:[方法名]_[场景]_[期望结果]
[Fact]
public async Task InitializeAsync_WithValidData_LoadsRoutesFromDatabase() { }
[Fact]
public async Task GetRoute_WithNonexistentTenant_ReturnsNull() { }
[Fact]
public async Task ReloadAsync_WhenCalled_RefreshesCache() { }
```
---
## 3. 单元测试模式
### 3.1 服务层测试示例
```csharp
// RouteCacheTests.cs
using Xunit;
using Moq;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
public class RouteCacheTests
{
private readonly Mock> _mockDbContextFactory;
private readonly Mock> _mockLogger;
private readonly RouteCache _sut; // System Under Test
public RouteCacheTests()
{
_mockDbContextFactory = new Mock>();
_mockLogger = new Mock>();
_sut = new RouteCache(_mockDbContextFactory.Object, _mockLogger.Object);
}
[Fact]
public async Task InitializeAsync_ShouldLoadRoutesFromDatabase()
{
// Arrange
var routes = new List
{
new() { Id = 1, ServiceName = "user-service", ClusterId = "user-cluster", IsGlobal = true }
};
var mockDbSet = CreateMockDbSet(routes);
var mockContext = new Mock();
mockContext.Setup(c => c.TenantRoutes).Returns(mockDbSet.Object);
_mockDbContextFactory
.Setup(f => f.CreateDbContext())
.Returns(mockContext.Object);
// Act
await _sut.InitializeAsync();
// Assert
var result = _sut.GetRoute("tenant1", "user-service");
result.Should().NotBeNull();
result!.ClusterId.Should().Be("user-cluster");
}
[Fact]
public async Task GetRoute_WhenTenantRouteExists_ReturnsTenantRoute()
{
// Arrange - 设置租户专用路由
// ...
// Act
var result = _sut.GetRoute("tenant1", "service1");
// Assert
result.Should().NotBeNull();
result!.IsGlobal.Should().BeFalse();
}
[Fact]
public async Task GetRoute_WhenNoTenantRouteButGlobalExists_ReturnsGlobalRoute()
{
// Arrange
// ...
// Act
var result = _sut.GetRoute("tenant-without-route", "global-service");
// Assert
result.Should().NotBeNull();
result!.IsGlobal.Should().BeTrue();
}
// 辅助方法:创建模拟 DbSet
private Mock> CreateMockDbSet(List data) where T : class
{
var queryable = data.AsQueryable();
var mockSet = new Mock>();
mockSet.As>().Setup(m => m.Provider).Returns(queryable.Provider);
mockSet.As>().Setup(m => m.Expression).Returns(queryable.Expression);
mockSet.As>().Setup(m => m.ElementType).Returns(queryable.ElementType);
mockSet.As>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator());
return mockSet;
}
}
```
### 3.2 中间件测试示例
```csharp
// TenantRoutingMiddlewareTests.cs
using Xunit;
using Moq;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
public class TenantRoutingMiddlewareTests
{
private readonly Mock _mockNext;
private readonly Mock _mockRouteCache;
private readonly Mock> _mockLogger;
private readonly TenantRoutingMiddleware _sut;
public TenantRoutingMiddlewareTests()
{
_mockNext = new Mock();
_mockRouteCache = new Mock();
_mockLogger = new Mock>();
_sut = new TenantRoutingMiddleware(_mockNext.Object, _mockRouteCache.Object, _mockLogger.Object);
}
[Fact]
public async Task InvokeAsync_WithoutTenantHeader_CallsNextWithoutProcessing()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.Path = "/api/user-service/users";
// Act
await _sut.InvokeAsync(context);
// Assert
_mockNext.Verify(n => n(context), Times.Once);
_mockRouteCache.Verify(r => r.GetRoute(It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task InvokeAsync_WithValidTenantAndRoute_SetsDynamicClusterId()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.Path = "/api/order-service/orders";
context.Request.Headers["X-Tenant-Id"] = "tenant-123";
var routeInfo = new RouteInfo
{
ClusterId = "order-cluster",
IsGlobal = false
};
_mockRouteCache
.Setup(r => r.GetRoute("tenant-123", "order-service"))
.Returns(routeInfo);
// Act
await _sut.InvokeAsync(context);
// Assert
context.Items["DynamicClusterId"].Should().Be("order-cluster");
_mockNext.Verify(n => n(context), Times.Once);
}
[Fact]
public async Task InvokeAsync_WithNoMatchingRoute_CallsNextWithoutClusterId()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.Path = "/api/unknown-service/data";
context.Request.Headers["X-Tenant-Id"] = "tenant-123";
_mockRouteCache
.Setup(r => r.GetRoute("tenant-123", "unknown-service"))
.Returns((RouteInfo?)null);
// Act
await _sut.InvokeAsync(context);
// Assert
context.Items.ContainsKey("DynamicClusterId").Should().BeFalse();
_mockNext.Verify(n => n(context), Times.Once);
}
}
```
### 3.3 控制器测试示例
```csharp
// GatewayConfigControllerTests.cs
using Xunit;
using Moq;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
public class GatewayConfigControllerTests
{
private readonly Mock> _mockDbFactory;
private readonly Mock _mockRouteProvider;
private readonly Mock _mockClusterProvider;
private readonly Mock _mockRouteCache;
private readonly GatewayConfigController _sut;
public GatewayConfigControllerTests()
{
_mockDbFactory = new Mock>();
_mockRouteProvider = new Mock();
_mockClusterProvider = new Mock();
_mockRouteCache = new Mock();
_sut = new GatewayConfigController(
_mockDbFactory.Object,
_mockRouteProvider.Object,
_mockClusterProvider.Object,
_mockRouteCache.Object
);
}
[Fact]
public async Task GetTenants_ShouldReturnPaginatedList()
{
// Arrange
var tenants = new List
{
new() { Id = 1, TenantCode = "tenant1", TenantName = "Tenant 1" },
new() { Id = 2, TenantCode = "tenant2", TenantName = "Tenant 2" }
};
// 设置模拟 DbContext...
// Act
var result = await _sut.GetTenants(page: 1, pageSize: 10);
// Assert
var okResult = result.Should().BeOfType().Subject;
var response = okResult.Value.Should().BeAnonymousType();
response.Property("total").Should().Be(2);
}
[Fact]
public async Task CreateTenant_WithValidData_ReturnsCreatedTenant()
{
// Arrange
var dto = new GatewayConfigController.CreateTenantDto
{
TenantCode = "new-tenant",
TenantName = "New Tenant"
};
// Act
var result = await _sut.CreateTenant(dto);
// Assert
var okResult = result.Should().BeOfType().Subject;
okResult.Value.Should().BeAssignableTo();
}
[Fact]
public async Task DeleteTenant_WithNonexistentId_ReturnsNotFound()
{
// Arrange
// 设置模拟返回 null
// Act
var result = await _sut.DeleteTenant(999);
// Assert
result.Should().BeOfType();
}
}
```
---
## 4. Mock 模式
### 4.1 接口 Mock
```csharp
// 使用 Moq 模拟接口
public class RouteCacheTests
{
private readonly Mock _mockRouteCache;
public RouteCacheTests()
{
_mockRouteCache = new Mock();
}
[Fact]
public async Task TestMethod()
{
// 设置返回值
_mockRouteCache
.Setup(r => r.GetRoute("tenant1", "service1"))
.Returns(new RouteInfo { ClusterId = "cluster1" });
// 设置异步方法
_mockRouteCache
.Setup(r => r.InitializeAsync())
.Returns(Task.CompletedTask);
// 验证调用
_mockRouteCache.Verify(r => r.GetRoute(It.IsAny(), It.IsAny()), Times.Once);
}
}
```
### 4.2 DbContext Mock
```csharp
// 使用 In-Memory 数据库进行测试
public class TestDatabaseFixture
{
public GatewayDbContext CreateContext()
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
var context = new GatewayDbContext(options);
// 种子数据
context.Tenants.Add(new GwTenant { Id = 1, TenantCode = "test-tenant" });
context.TenantRoutes.Add(new GwTenantRoute
{
Id = 1,
ServiceName = "test-service",
ClusterId = "test-cluster"
});
context.SaveChanges();
return context;
}
}
public class GatewayDbContextTests : IClassFixture
{
private readonly TestDatabaseFixture _fixture;
public GatewayDbContextTests(TestDatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task SaveChangesAsync_ShouldNotifyConfigChange()
{
// Arrange
await using var context = _fixture.CreateContext();
// Act
var route = new GwTenantRoute { ServiceName = "new-service", ClusterId = "new-cluster" };
context.TenantRoutes.Add(route);
await context.SaveChangesAsync();
// Assert
// 验证通知行为(如果需要)
}
}
```
### 4.3 Redis Mock
```csharp
// 使用 Moq 模拟 Redis
public class RedisConnectionManagerTests
{
private readonly Mock _mockRedis;
private readonly Mock _mockDatabase;
public RedisConnectionManagerTests()
{
_mockRedis = new Mock();
_mockDatabase = new Mock();
_mockRedis.Setup(r => r.GetDatabase(It.IsAny(), It.IsAny