API 文档与测试策略
大约 9 分钟约 2714 字
API 文档与测试策略
简介
良好的 API 文档和完善的测试是高质量 API 的基础。Swagger/OpenAPI 提供标准化的 API 文档,单元测试、集成测试和契约测试保障 API 的正确性。理解文档生成配置和测试金字塔策略,有助于构建可维护的 API 服务。
特点
Swagger 配置
OpenAPI 文档生成
// dotnet add package Swashbuckle.AspNetCore
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My API",
Version = "v1",
Description = "电商平台 API 服务",
Contact = new OpenApiContact
{
Name = "API Support",
Email = "support@example.com"
}
});
// 包含 XML 注释
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
// JWT 认证按钮
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header. Example: \"Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
// 自定义 Schema ID
options.CustomSchemaIds(type => type.FullName?.Replace("+", "."));
// 排序
options.OrderActionsBy(desc => desc.GroupName);
options.TagActionsBy(desc => new[] { desc.GroupName ?? "Default" });
});
var app = builder.Build();
app.UseSwagger(options =>
{
options.RouteTemplate = "docs/{documentName}/openapi.json";
});
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/docs/v1/openapi.json", "My API V1");
options.RoutePrefix = "docs";
options.DocumentTitle = "My API Docs";
options.DisplayRequestDuration = true;
options.EnableDeepLinking = true;
});Minimal API 文档注解
// Minimal API 注解
app.MapGet("/api/users", async (AppDbContext db, int page = 1, int pageSize = 20) =>
{
var users = await db.Users
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(u => new UserDto(u.Id, u.Name, u.Email))
.ToListAsync();
return Results.Ok(users);
})
.WithName("GetUsers")
.WithTags("Users")
.WithSummary("获取用户列表")
.WithDescription("分页获取所有用户信息")
.WithOpenApi(op =>
{
op.Parameters[0].Description = "页码(从 1 开始)";
op.Parameters[1].Description = "每页数量(最大 100)";
return op;
})
.Produces<List<UserDto>>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status500InternalServerError);
app.MapPost("/api/users", async (CreateUserRequest request, AppDbContext db) =>
{
var user = new User { Name = request.Name, Email = request.Email };
db.Users.Add(user);
await db.SaveChangesAsync();
return Results.Created($"/api/users/{user.Id}", new UserDto(user.Id, user.Name, user.Email));
})
.WithName("CreateUser")
.WithTags("Users")
.WithSummary("创建用户")
.Produces<UserDto>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status409Conflict);
// DTO 注释
/// <summary>
/// 创建用户请求
/// </summary>
public record CreateUserRequest(
/// <summary>
/// 用户名称
/// </summary>
/// <example>张三</example>
[Required][StringLength(100)] string Name,
/// <summary>
/// 邮箱地址
/// </summary>
/// <example>zhangsan@example.com</example>
[Required][EmailAddress] string Email);单元测试
xUnit + Moq
// dotnet add package xunit
// dotnet add package Moq
// dotnet add package FluentAssertions
// 被测服务
public class OrderService
{
private readonly IOrderRepository _orderRepo;
private readonly IInventoryService _inventoryService;
private readonly IEventBus _eventBus;
public OrderService(
IOrderRepository orderRepo,
IInventoryService inventoryService,
IEventBus eventBus)
{
_orderRepo = orderRepo;
_inventoryService = inventoryService;
_eventBus = eventBus;
}
public async Task<OrderDto> CreateOrderAsync(CreateOrderCommand command)
{
// 验证库存
var hasStock = await _inventoryService.CheckStockAsync(command.ProductId, command.Quantity);
if (!hasStock) throw new BusinessException("库存不足");
var order = new Order
{
Id = Guid.NewGuid(),
ProductId = command.ProductId,
Quantity = command.Quantity,
Status = OrderStatus.Created
};
await _orderRepo.SaveAsync(order);
await _inventoryService.ReserveAsync(command.ProductId, command.Quantity);
await _eventBus.PublishAsync(new OrderCreatedEvent(order.Id));
return new OrderDto(order.Id, order.ProductId, order.Quantity, order.Status);
}
}
// 单元测试
public class OrderServiceTests
{
private readonly Mock<IOrderRepository> _orderRepoMock;
private readonly Mock<IInventoryService> _inventoryMock;
private readonly Mock<IEventBus> _eventBusMock;
private readonly OrderService _service;
public OrderServiceTests()
{
_orderRepoMock = new Mock<IOrderRepository>();
_inventoryMock = new Mock<IInventoryService>();
_eventBusMock = new Mock<IEventBus>();
_service = new OrderService(_orderRepoMock.Object, _inventoryMock.Object, _eventBusMock.Object);
}
[Fact]
public async Task CreateOrder_WithStock_ReturnsOrder()
{
// Arrange
var command = new CreateOrderCommand(Guid.NewGuid(), 10);
_inventoryMock.Setup(x => x.CheckStockAsync(command.ProductId, command.Quantity))
.ReturnsAsync(true);
// Act
var result = await _service.CreateOrderAsync(command);
// Assert
result.Should().NotBeNull();
result.Status.Should().Be(OrderStatus.Created);
// 验证交互
_orderRepoMock.Verify(x => x.SaveAsync(It.IsAny<Order>()), Times.Once);
_inventoryMock.Verify(x => x.ReserveAsync(command.ProductId, command.Quantity), Times.Once);
_eventBusMock.Verify(x => x.PublishAsync(It.IsAny<OrderCreatedEvent>()), Times.Once);
}
[Fact]
public async Task CreateOrder_NoStock_ThrowsException()
{
// Arrange
var command = new CreateOrderCommand(Guid.NewGuid(), 10);
_inventoryMock.Setup(x => x.CheckStockAsync(command.ProductId, command.Quantity))
.ReturnsAsync(false);
// Act & Assert
await Assert.ThrowsAsync<BusinessException>(() => _service.CreateOrderAsync(command));
// 验证未保存
_orderRepoMock.Verify(x => x.SaveAsync(It.IsAny<Order>()), Times.Never);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public async Task CreateOrder_InvalidQuantity_ThrowsException(int quantity)
{
var command = new CreateOrderCommand(Guid.NewGuid(), quantity);
await Assert.ThrowsAsync<ArgumentException>(() => _service.CreateOrderAsync(command));
}
}集成测试
WebApplicationFactory
// 集成测试 — 测试完整请求管道
public class OrderApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>, IAsyncLifetime
{
private readonly WebApplicationFactory<Program> _factory;
private readonly IServiceScope _scope;
private readonly HttpClient _client;
private readonly AppDbContext _db;
public OrderApiIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// 替换数据库
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null) services.Remove(descriptor);
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("IntegrationTestDb"));
// 替换外部服务
services.RemoveAll<IInventoryService>();
services.AddScoped<IInventoryService, FakeInventoryService>();
});
builder.UseEnvironment("Testing");
});
_scope = _factory.Services.CreateScope();
_client = _factory.CreateClient();
_db = _scope.ServiceProvider.GetRequiredService<AppDbContext>();
}
[Fact]
public async Task CreateOrder_ValidRequest_ReturnsCreated()
{
// Arrange
var request = new CreateUserRequest("Test User", "test@example.com");
var content = JsonContent.Create(request);
// Act
var response = await _client.PostAsync("/api/users", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
response.Headers.Location.Should().NotBeNull();
var user = await response.Content.ReadFromJsonAsync<UserDto>();
user.Should().NotBeNull();
user!.Name.Should().Be("Test User");
// 验证数据库
var dbUser = await _db.Users.FindAsync(user.Id);
dbUser.Should().NotBeNull();
}
[Fact]
public async Task CreateOrder_DuplicateEmail_ReturnsConflict()
{
// 预设数据
_db.Users.Add(new User { Name = "Existing", Email = "test@example.com" });
await _db.SaveChangesAsync();
var request = new CreateUserRequest("New User", "test@example.com");
var response = await _client.PostAsJsonAsync("/api/users", request);
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
}
[Fact]
public async Task GetUser_NotFound_Returns404()
{
var response = await _client.GetAsync("/api/users/999");
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
await _db.Database.EnsureDeletedAsync();
_scope.Dispose();
}
}
// Fake 服务
public class FakeInventoryService : IInventoryService
{
public Task<bool> CheckStockAsync(Guid productId, int quantity) => Task.FromResult(true);
public Task ReserveAsync(Guid productId, int quantity) => Task.CompletedTask;
}契约测试
消费者驱动契约
// dotnet add package PactNet
// 消费者测试(Order Service 消费 User Service)
public class UserServiceContractTests : IDisposable
{
private readonly PactVerifier _pactVerifier;
private readonly WebApplicationFactory<Program> _factory;
[Fact]
public void VerifyUserApiContract()
{
// 验证提供者是否满足契约
_pactVerifier
.ServiceProvider("User Service", _factory.Server)
.HonoursPactWith("Order Service")
.PactUri(@"./pacts/order-service-user-service.json")
.Verify();
}
public void Dispose()
{
_factory?.Dispose();
}
}
// 消费者端定义契约
public class OrderServiceConsumerTests
{
[Fact]
public async Task GetUser_ExistingUser_Returns200()
{
// 定义期望的契约
var pact = new ConsumerPactBuilder("Order Service", "User Service")
.HasPactWith("User Service")
.UponReceiving("A request to get user")
.WithRequest(HttpMethod.Get, "/api/users/123")
.WillRespond()
.WithStatus(200)
.WithHeader("Content-Type", "application/json")
.WithJsonBody(new
{
id = 123,
name = "Test User",
email = "test@example.com"
})
.RegisterPact();
}
}优点
缺点
总结
Swagger/OpenAPI 通过 AddSwaggerGen() 自动生成 API 文档,支持 XML 注释增强描述、JWT 认证按钮和自定义 Schema。单元测试使用 xUnit + Moq 隔离依赖,FluentAssertions 提供流畅的断言语法。集成测试使用 WebApplicationFactory 测试完整请求管道,替换数据库和外部服务。测试金字塔建议:70% 单元测试、20% 集成测试、10% 端到端测试。契约测试确保服务间 API 兼容性。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《API 文档与测试策略》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《API 文档与测试策略》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《API 文档与测试策略》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《API 文档与测试策略》最大的收益和代价分别是什么?
性能测试与负载测试
使用 NBomber 进行 API 负载测试
// dotnet add package NBomber
using NBomber.CSharp;
using NBomber.Http.CSharp;
public class ApiLoadTests
{
public static void RunLoadTest()
{
var httpFactory = HttpClientFactory.Create();
var scenario = Scenario.Create("api_load_test", async context =>
{
var request = Http.CreateRequest("GET", "https://api.example.com/api/users")
.WithHeader("Authorization", "Bearer test-token");
var response = await Http.Send(httpFactory, request);
return response.StatusCode == 200 ? Response.Ok() : Response.Fail();
})
.WithWarmUpDuration(TimeSpan.FromSeconds(5))
.WithLoadSimulations(
Simulation.Inject(perSec: 100, during: TimeSpan.FromSeconds(30)),
Simulation.KeepConstant(copies: 50, during: TimeSpan.FromMinutes(1))
);
NBomberRunner
.RegisterScenarios(scenario)
.Run();
}
}单元测试最佳实践
// 测试命名规范:方法名_场景_预期结果
public class OrderServiceTests
{
private readonly Mock<IOrderRepository> _orderRepoMock;
private readonly Mock<IInventoryService> _inventoryMock;
private readonly Mock<IEventBus> _eventBusMock;
private readonly OrderService _service;
public OrderServiceTests()
{
_orderRepoMock = new Mock<IOrderRepository>();
_inventoryMock = new Mock<IInventoryService>();
_eventBusMock = new Mock<IEventBus>();
_service = new OrderService(
_orderRepoMock.Object,
_inventoryMock.Object,
_eventBusMock.Object);
}
// 使用 Theory 进行参数化测试
[Theory]
[InlineData(0, "商品数量不能为零")]
[InlineData(-1, "商品数量不能为负数")]
[InlineData(-100, "商品数量不能为负数")]
public async Task CreateOrder_InvalidQuantity_ThrowsBusinessException(
int quantity, string expectedMessage)
{
// Arrange
var command = new CreateOrderCommand(Guid.NewGuid(), quantity);
// Act
var ex = await Assert.ThrowsAsync<BusinessException>(
() => _service.CreateOrderAsync(command));
// Assert
ex.Message.Should().Contain(expectedMessage);
}
// 测试边界条件
[Fact]
public async Task CreateOrder_MaxQuantity_Succeeds()
{
var command = new CreateOrderCommand(Guid.NewGuid(), 999);
_inventoryMock.Setup(x => x.CheckStockAsync(It.IsAny<Guid>(), It.IsAny<int>()))
.ReturnsAsync(true);
var result = await _service.CreateOrderAsync(command);
result.Should().NotBeNull();
result.Quantity.Should().Be(999);
}
// 测试并发场景
[Fact]
public async Task CreateOrder_ConcurrentRequests_OnlyOneSucceeds()
{
var command = new CreateOrderCommand(Guid.NewGuid(), 10);
var callCount = 0;
_inventoryMock.Setup(x => x.CheckStockAsync(It.IsAny<Guid>(), 10))
.Callback(() =>
{
callCount++;
if (callCount > 1) throw new ConcurrencyException("库存已被占用");
})
.ReturnsAsync(true);
// 第一次调用应该成功
var result1 = await _service.CreateOrderAsync(command);
result1.Should().NotBeNull();
}
// 测试事件发布
[Fact]
public async Task CreateOrder_Success_PublishesOrderCreatedEvent()
{
var command = new CreateOrderCommand(Guid.NewGuid(), 5);
_inventoryMock.Setup(x => x.CheckStockAsync(It.IsAny<Guid>(), 5))
.ReturnsAsync(true);
await _service.CreateOrderAsync(command);
_eventBusMock.Verify(
x => x.PublishAsync(It.Is<OrderCreatedEvent>(e => e.OrderId != Guid.Empty)),
Times.Once);
}
}集成测试数据库策略
// 使用 Testcontainers 实现真实数据库集成测试
// dotnet add package Testcontainers
// dotnet add package Testcontainers.MsSql
public class OrderApiWithTestcontainersTests : IAsyncLifetime
{
private readonly MsSqlContainer _dbContainer = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
private WebApplicationFactory<Program> _factory = null!;
private HttpClient _client = null!;
public async Task InitializeAsync()
{
await _dbContainer.StartAsync();
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// 替换为 Testcontainers 的连接字符串
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null) services.Remove(descriptor);
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(_dbContainer.GetConnectionString()));
// 自动运行迁移
using var scope = services.BuildServiceProvider().CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.EnsureCreated();
});
});
_client = _factory.CreateClient();
}
[Fact]
public async Task CreateOrder_WithRealDb_ReturnsCreated()
{
var request = new CreateOrderRequest(productId: "P001", quantity: 5);
var response = await _client.PostAsJsonAsync("/api/orders", request);
response.StatusCode.Should().Be(HttpStatusCode.Created);
var result = await response.Content.ReadFromJsonAsync<OrderDto>();
result!.Quantity.Should().Be(5);
}
public async Task DisposeAsync()
{
_factory?.Dispose();
await _dbContainer.DisposeAsync();
}
}API 版本控制与文档版本化
// dotnet add package Asp.Versioning.Mvc
// dotnet add package Asp.Versioning.Mvc.ApiExplorer
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("X-Api-Version"),
new QueryStringApiVersionReader("api-version")
);
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// 控制器版本
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class OrdersV1Controller : ControllerBase { }
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class OrdersV2Controller : ControllerBase { }