前言
在现代软件开发中,API(应用程序编程接口)已成为不同服务和应用程序之间通信的桥梁。一个优秀的API不仅需要具备良好的功能性,更需要有完善的文档和全面的测试策略。本文将深入探讨ASP.NET Core环境下的API文档生成与测试实践,帮助开发者构建更加健壮和易于维护的API服务。
文章目录
- 前言
-
- API文档的重要性
- Swagger/OpenAPI集成
- API版本控制策略
- 单元测试与集成测试
- 高级测试技巧
-
- API测试自动化流程
- [契约测试(Contract Testing)](#契约测试(Contract Testing))
- API文档一致性测试
- 负载测试和压力测试
- 性能与安全测试
- 最佳实践总结
- 学习资源与工具推荐
- 总结
API文档的重要性
API文档是开发团队协作的核心工具,它不仅服务于外部开发者,更是内部团队维护和扩展API的重要依据。良好的API文档应该具备以下特征:
- 完整性:涵盖所有API端点、参数、响应格式
- 准确性:与实际API行为保持一致
- 可读性:清晰的描述和示例
- 交互性:支持在线测试功能
API开发 代码注释 自动生成文档 Swagger UI 开发者使用 反馈改进
Swagger/OpenAPI集成
Swagger(现称为OpenAPI)是目前最流行的API文档规范。在ASP.NET Core中,我们可以通过Swashbuckle.AspNetCore包轻松集成Swagger功能。
基础配置
首先,安装必要的NuGet包:
bash
# 安装Swagger相关包
dotnet add package Swashbuckle.AspNetCore
dotnet add package Swashbuckle.AspNetCore.Annotations
在Program.cs
中配置Swagger服务:
csharp
using Microsoft.OpenApi.Models;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
// 添加控制器服务
builder.Services.AddControllers();
// 配置Swagger服务
builder.Services.AddSwaggerGen(options =>
{
// 基本信息配置
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "示例API",
Description = "一个ASP.NET Core Web API的完整示例",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "技术支持",
Url = new Uri("https://example.com/contact"),
Email = "support@example.com"
},
License = new OpenApiLicense
{
Name = "MIT License",
Url = new Uri("https://example.com/license")
}
});
// 启用XML注释
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
// 启用注解功能
options.EnableAnnotations();
// 配置JWT认证
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. 格式:\"Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});
var app = builder.Build();
// 配置HTTP请求管道
if (app.Environment.IsDevelopment())
{
// 启用Swagger中间件
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "示例API v1");
options.RoutePrefix = string.Empty; // 设置Swagger UI在根路径
options.DocumentTitle = "API文档中心";
// 自定义CSS样式
options.InjectStylesheet("/swagger-ui/custom.css");
// 启用深色主题
options.DefaultModelsExpandDepth(-1);
options.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
});
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
XML注释文档生成
为了生成详细的API文档,需要在项目文件中启用XML注释:
xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- 启用XML文档生成 -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- 忽略缺少XML注释的警告 -->
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
</ItemGroup>
</Project>
控制器注释示例
创建一个带有详细注释的控制器:
csharp
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using System.ComponentModel.DataAnnotations;
namespace ApiDocumentationExample.Controllers;
/// <summary>
/// 用户管理相关API
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
[SwaggerTag("用户管理:提供用户的增删改查功能")]
public class UsersController : ControllerBase
{
/// <summary>
/// 获取用户列表
/// </summary>
/// <param name="page">页码,从1开始</param>
/// <param name="size">每页数量,默认10条</param>
/// <param name="keyword">搜索关键词,可选</param>
/// <returns>分页用户列表</returns>
/// <response code="200">成功返回用户列表</response>
/// <response code="400">请求参数错误</response>
/// <response code="500">服务器内部错误</response>
[HttpGet]
[SwaggerOperation(Summary = "获取用户列表", Description = "支持分页和关键词搜索的用户列表查询")]
[SwaggerResponse(200, "查询成功", typeof(PagedResult<UserDto>))]
[SwaggerResponse(400, "参数错误", typeof(ErrorResponse))]
[SwaggerResponse(500, "服务器错误", typeof(ErrorResponse))]
public async Task<ActionResult<PagedResult<UserDto>>> GetUsers(
[FromQuery, Range(1, int.MaxValue)] int page = 1,
[FromQuery, Range(1, 100)] int size = 10,
[FromQuery] string? keyword = null)
{
try
{
// 模拟数据查询逻辑
var users = GenerateSampleUsers(page, size, keyword);
return Ok(new PagedResult<UserDto>
{
Data = users,
Total = 100, // 模拟总数
Page = page,
Size = size
});
}
catch (Exception ex)
{
return StatusCode(500, new ErrorResponse
{
Message = "查询用户列表失败",
Details = ex.Message
});
}
}
/// <summary>
/// 根据ID获取用户详情
/// </summary>
/// <param name="id">用户唯一标识符</param>
/// <returns>用户详细信息</returns>
/// <response code="200">成功返回用户信息</response>
/// <response code="404">用户不存在</response>
[HttpGet("{id:int}")]
[SwaggerOperation(Summary = "获取用户详情", Description = "通过用户ID获取详细信息")]
[SwaggerResponse(200, "获取成功", typeof(UserDto))]
[SwaggerResponse(404, "用户不存在", typeof(ErrorResponse))]
public async Task<ActionResult<UserDto>> GetUser(
[FromRoute, SwaggerParameter("用户ID", Required = true)] int id)
{
// 模拟用户查询
if (id <= 0 || id > 1000)
{
return NotFound(new ErrorResponse
{
Message = "用户不存在",
Details = $"ID为{id}的用户未找到"
});
}
return Ok(new UserDto
{
Id = id,
Username = $"user_{id}",
Email = $"user_{id}@example.com",
FullName = $"用户 {id}",
CreatedAt = DateTime.Now.AddDays(-30),
IsActive = true
});
}
/// <summary>
/// 创建新用户
/// </summary>
/// <param name="request">用户创建请求</param>
/// <returns>创建的用户信息</returns>
/// <response code="201">用户创建成功</response>
/// <response code="400">请求数据无效</response>
/// <response code="409">用户名或邮箱已存在</response>
[HttpPost]
[SwaggerOperation(Summary = "创建用户", Description = "创建新的用户账户")]
[SwaggerResponse(201, "创建成功", typeof(UserDto))]
[SwaggerResponse(400, "数据无效", typeof(ErrorResponse))]
[SwaggerResponse(409, "冲突", typeof(ErrorResponse))]
public async Task<ActionResult<UserDto>> CreateUser(
[FromBody, SwaggerRequestBody("用户创建信息", Required = true)] CreateUserRequest request)
{
// 模拟数据验证
if (!ModelState.IsValid)
{
return BadRequest(new ErrorResponse
{
Message = "请求数据无效",
Details = string.Join(", ", ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage))
});
}
// 模拟重复检查
if (request.Username.Contains("admin"))
{
return Conflict(new ErrorResponse
{
Message = "用户名已存在",
Details = "该用户名已被使用,请选择其他用户名"
});
}
// 模拟创建用户
var newUser = new UserDto
{
Id = new Random().Next(1001, 9999),
Username = request.Username,
Email = request.Email,
FullName = request.FullName,
CreatedAt = DateTime.Now,
IsActive = true
};
return CreatedAtAction(nameof(GetUser), new { id = newUser.Id }, newUser);
}
// 模拟数据生成方法
private List<UserDto> GenerateSampleUsers(int page, int size, string? keyword)
{
return Enumerable.Range((page - 1) * size + 1, size)
.Select(i => new UserDto
{
Id = i,
Username = $"user_{i}",
Email = $"user_{i}@example.com",
FullName = $"用户 {i}",
CreatedAt = DateTime.Now.AddDays(-i),
IsActive = i % 2 == 0
})
.Where(u => string.IsNullOrEmpty(keyword) ||
u.Username.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
u.FullName.Contains(keyword, StringComparison.OrdinalIgnoreCase))
.ToList();
}
}
/// <summary>
/// 用户数据传输对象
/// </summary>
[SwaggerSchema(Description = "用户基本信息")]
public class UserDto
{
/// <summary>
/// 用户唯一标识符
/// </summary>
[SwaggerSchema("用户ID", ReadOnly = true)]
public int Id { get; set; }
/// <summary>
/// 用户名(登录名)
/// </summary>
[SwaggerSchema("用户名", Example = "john_doe")]
public string Username { get; set; } = string.Empty;
/// <summary>
/// 电子邮箱地址
/// </summary>
[SwaggerSchema("邮箱地址", Example = "john.doe@example.com")]
public string Email { get; set; } = string.Empty;
/// <summary>
/// 用户真实姓名
/// </summary>
[SwaggerSchema("真实姓名", Example = "张三")]
public string FullName { get; set; } = string.Empty;
/// <summary>
/// 账户创建时间
/// </summary>
[SwaggerSchema("创建时间", ReadOnly = true)]
public DateTime CreatedAt { get; set; }
/// <summary>
/// 账户是否激活
/// </summary>
[SwaggerSchema("激活状态", Example = true)]
public bool IsActive { get; set; }
}
/// <summary>
/// 创建用户请求模型
/// </summary>
[SwaggerSchema(Description = "创建用户所需的信息")]
public class CreateUserRequest
{
/// <summary>
/// 用户名,3-20个字符,只能包含字母、数字和下划线
/// </summary>
[Required(ErrorMessage = "用户名是必填项")]
[StringLength(20, MinimumLength = 3, ErrorMessage = "用户名长度必须在3-20个字符之间")]
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "用户名只能包含字母、数字和下划线")]
[SwaggerSchema("用户名", Example = "john_doe")]
public string Username { get; set; } = string.Empty;
/// <summary>
/// 电子邮箱地址
/// </summary>
[Required(ErrorMessage = "邮箱地址是必填项")]
[EmailAddress(ErrorMessage = "邮箱地址格式不正确")]
[SwaggerSchema("邮箱地址", Example = "john.doe@example.com")]
public string Email { get; set; } = string.Empty;
/// <summary>
/// 用户真实姓名
/// </summary>
[Required(ErrorMessage = "真实姓名是必填项")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "姓名长度必须在2-50个字符之间")]
[SwaggerSchema("真实姓名", Example = "张三")]
public string FullName { get; set; } = string.Empty;
/// <summary>
/// 密码,至少8个字符,包含字母和数字
/// </summary>
[Required(ErrorMessage = "密码是必填项")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "密码长度必须在8-100个字符之间")]
[RegularExpression(@"^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$",
ErrorMessage = "密码必须包含至少一个字母和一个数字")]
[SwaggerSchema("密码", Example = "password123")]
public string Password { get; set; } = string.Empty;
}
/// <summary>
/// 分页查询结果
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
[SwaggerSchema(Description = "分页查询结果")]
public class PagedResult<T>
{
/// <summary>
/// 数据列表
/// </summary>
[SwaggerSchema("数据列表")]
public List<T> Data { get; set; } = new();
/// <summary>
/// 总记录数
/// </summary>
[SwaggerSchema("总记录数", Example = 100)]
public int Total { get; set; }
/// <summary>
/// 当前页码
/// </summary>
[SwaggerSchema("当前页码", Example = 1)]
public int Page { get; set; }
/// <summary>
/// 每页大小
/// </summary>
[SwaggerSchema("每页大小", Example = 10)]
public int Size { get; set; }
/// <summary>
/// 总页数
/// </summary>
[SwaggerSchema("总页数", ReadOnly = true)]
public int TotalPages => (int)Math.Ceiling((double)Total / Size);
/// <summary>
/// 是否有下一页
/// </summary>
[SwaggerSchema("是否有下一页", ReadOnly = true)]
public bool HasNext => Page < TotalPages;
/// <summary>
/// 是否有上一页
/// </summary>
[SwaggerSchema("是否有上一页", ReadOnly = true)]
public bool HasPrevious => Page > 1;
}
/// <summary>
/// 错误响应模型
/// </summary>
[SwaggerSchema(Description = "API错误响应")]
public class ErrorResponse
{
/// <summary>
/// 错误消息
/// </summary>
[SwaggerSchema("错误消息", Example = "操作失败")]
public string Message { get; set; } = string.Empty;
/// <summary>
/// 详细错误信息
/// </summary>
[SwaggerSchema("详细信息", Example = "具体的错误原因描述")]
public string? Details { get; set; }
/// <summary>
/// 错误代码
/// </summary>
[SwaggerSchema("错误代码", Example = "USER_NOT_FOUND")]
public string? ErrorCode { get; set; }
/// <summary>
/// 错误发生时间
/// </summary>
[SwaggerSchema("发生时间", ReadOnly = true)]
public DateTime Timestamp { get; set; } = DateTime.Now;
}
API版本控制策略
API版本控制是确保API向后兼容性和演进能力的关键策略。在ASP.NET Core中,我们可以通过多种方式实现API版本控制。
API v1.0 API v1.1 API v2.0 客户端A 客户端B 客户端C
版本控制策略对比
API版本控制策略 URL路径版本控制 查询参数版本控制 请求头版本控制 媒体类型版本控制 /api/v1/users /api/v2/users ?version=1.0 ?api-version=2.0 X-API-Version: 1.0 Accept-Version: 2.0 application/vnd.api+json;version=1 application/vnd.api+json;version=2
版本控制实现
首先安装版本控制相关的NuGet包:
bash
# 安装API版本控制包(新版本)
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
在Program.cs
中配置版本控制:
csharp
using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
var builder = WebApplication.CreateBuilder(args);
// 添加控制器服务
builder.Services.AddControllers();
// 配置API版本控制
var apiVersioning = builder.Services.AddApiVersioning(options =>
{
// 设置默认版本
options.DefaultApiVersion = new ApiVersion(1, 0);
// 当客户端未指定版本时,使用默认版本
options.AssumeDefaultVersionWhenUnspecified = true;
// 在响应头中返回支持的版本信息
options.ReportApiVersions = true;
// 配置版本读取方式(支持多种方式)
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(), // URL路径:/api/v1/users
new QueryStringApiVersionReader("version"), // 查询参数:?version=1.0
new HeaderApiVersionReader("X-API-Version"), // 请求头:X-API-Version: 1.0
new MediaTypeApiVersionReader("ver") // 媒体类型:application/json;ver=1.0
);
});
// 添加API资源管理器(用于Swagger文档生成)
apiVersioning.AddApiExplorer(setup =>
{
// 设置版本名称格式
setup.GroupNameFormat = "'v'VVV";
// 在URL中替换版本占位符
setup.SubstituteApiVersionInUrl = true;
});
// 配置Swagger支持多版本
builder.Services.AddSwaggerGen(options =>
{
// 基本配置...
var provider = builder.Services.BuildServiceProvider()
.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, new OpenApiInfo
{
Title = "示例API",
Version = description.ApiVersion.ToString(),
Description = description.IsDeprecated ? "此版本已弃用" : "当前版本"
});
}
});
var app = builder.Build();
// 配置Swagger UI支持多版本
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
$"示例API {description.GroupName.ToUpperInvariant()}");
}
});
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
版本化控制器示例
创建不同版本的控制器:
csharp
namespace ApiDocumentationExample.Controllers.V1;
/// <summary>
/// 用户管理API v1.0
/// </summary>
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[SwaggerTag("用户管理v1.0:基础用户管理功能")]
public class UsersController : ControllerBase
{
/// <summary>
/// 获取用户列表(v1.0版本)
/// </summary>
/// <param name="page">页码</param>
/// <param name="size">每页大小</param>
/// <returns>用户列表</returns>
[HttpGet]
[MapToApiVersion("1.0")]
[SwaggerOperation(Summary = "获取用户列表", Description = "v1.0版本的用户列表查询,基础功能")]
public async Task<ActionResult<List<UserV1Dto>>> GetUsers(
[FromQuery] int page = 1,
[FromQuery] int size = 10)
{
// v1.0版本的实现:只返回基本信息
var users = Enumerable.Range((page - 1) * size + 1, size)
.Select(i => new UserV1Dto
{
Id = i,
Name = $"用户 {i}",
Email = $"user_{i}@example.com"
})
.ToList();
return Ok(users);
}
/// <summary>
/// 获取用户详情(v1.0版本)
/// </summary>
/// <param name="id">用户ID</param>
/// <returns>用户详情</returns>
[HttpGet("{id:int}")]
[MapToApiVersion("1.0")]
public async Task<ActionResult<UserV1Dto>> GetUser(int id)
{
return Ok(new UserV1Dto
{
Id = id,
Name = $"用户 {id}",
Email = $"user_{id}@example.com"
});
}
}
namespace ApiDocumentationExample.Controllers.V2;
/// <summary>
/// 用户管理API v2.0
/// </summary>
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[SwaggerTag("用户管理v2.0:增强的用户管理功能")]
public class UsersController : ControllerBase
{
/// <summary>
/// 获取用户列表(v2.0版本)
/// </summary>
/// <param name="page">页码</param>
/// <param name="size">每页大小</param>
/// <param name="keyword">搜索关键词</param>
/// <param name="sortBy">排序字段</param>
/// <param name="sortOrder">排序方向</param>
/// <returns>增强的用户列表</returns>
[HttpGet]
[MapToApiVersion("2.0")]
[SwaggerOperation(Summary = "获取用户列表", Description = "v2.0版本新增搜索和排序功能")]
public async Task<ActionResult<PagedResult<UserV2Dto>>> GetUsers(
[FromQuery] int page = 1,
[FromQuery] int size = 10,
[FromQuery] string? keyword = null,
[FromQuery] string sortBy = "id",
[FromQuery] string sortOrder = "asc")
{
// v2.0版本的实现:增加搜索、排序和更多字段
var users = Enumerable.Range((page - 1) * size + 1, size)
.Select(i => new UserV2Dto
{
Id = i,
Username = $"user_{i}",
Email = $"user_{i}@example.com",
FullName = $"用户 {i}",
Avatar = $"https://avatar.example.com/{i}.jpg",
Status = i % 2 == 0 ? "active" : "inactive",
CreatedAt = DateTime.Now.AddDays(-i),
LastLoginAt = DateTime.Now.AddHours(-i),
Profile = new UserProfileDto
{
Bio = $"这是用户{i}的个人简介",
Location = "中国",
Website = $"https://user{i}.example.com"
}
})
.ToList();
return Ok(new PagedResult<UserV2Dto>
{
Data = users,
Total = 1000,
Page = page,
Size = size
});
}
/// <summary>
/// 获取用户详情(v2.0版本)
/// </summary>
/// <param name="id">用户ID</param>
/// <param name="includeProfile">是否包含详细资料</param>
/// <returns>用户详情</returns>
[HttpGet("{id:int}")]
[MapToApiVersion("2.0")]
public async Task<ActionResult<UserV2Dto>> GetUser(
int id,
[FromQuery] bool includeProfile = true)
{
var user = new UserV2Dto
{
Id = id,
Username = $"user_{id}",
Email = $"user_{id}@example.com",
FullName = $"用户 {id}",
Avatar = $"https://avatar.example.com/{id}.jpg",
Status = "active",
CreatedAt = DateTime.Now.AddDays(-30),
LastLoginAt = DateTime.Now.AddHours(-2)
};
if (includeProfile)
{
user.Profile = new UserProfileDto
{
Bio = $"这是用户{id}的个人简介",
Location = "中国",
Website = $"https://user{id}.example.com"
};
}
return Ok(user);
}
/// <summary>
/// 批量操作用户(v2.0新增功能)
/// </summary>
/// <param name="request">批量操作请求</param>
/// <returns>操作结果</returns>
[HttpPost("batch")]
[MapToApiVersion("2.0")]
[SwaggerOperation(Summary = "批量操作用户", Description = "v2.0新增的批量操作功能")]
public async Task<ActionResult<BatchOperationResult>> BatchOperation(
[FromBody] BatchUserOperationRequest request)
{
return Ok(new BatchOperationResult
{
SuccessCount = request.UserIds.Count,
FailureCount = 0,
Operation = request.Operation,
Timestamp = DateTime.Now
});
}
}
// 版本弃用示例
namespace ApiDocumentationExample.Controllers.V3;
/// <summary>
/// 用户管理API v3.0(预览版)
/// </summary>
[ApiController]
[ApiVersion("3.0-preview")]
[Route("api/v{version:apiVersion}/[controller]")]
[SwaggerTag("用户管理v3.0:预览版本")]
public class UsersController : ControllerBase
{
/// <summary>
/// 获取用户列表(v3.0预览版)
/// </summary>
[HttpGet]
[MapToApiVersion("3.0-preview")]
[SwaggerOperation(Summary = "获取用户列表", Description = "v3.0预览版,引入GraphQL风格查询")]
public async Task<ActionResult> GetUsers([FromQuery] string? fields = null)
{
return Ok(new { message = "v3.0预览版功能开发中" });
}
}
// 弃用旧版本
namespace ApiDocumentationExample.Controllers.Legacy;
/// <summary>
/// 用户管理API v0.9(已弃用)
/// </summary>
[ApiController]
[ApiVersion("0.9", Deprecated = true)]
[Route("api/v{version:apiVersion}/[controller]")]
[SwaggerTag("用户管理v0.9:已弃用版本")]
public class UsersController : ControllerBase
{
/// <summary>
/// 获取用户列表(已弃用)
/// </summary>
[HttpGet]
[MapToApiVersion("0.9")]
[SwaggerOperation(Summary = "获取用户列表", Description = "此版本已弃用,请使用v1.0或更高版本")]
[Obsolete("此API版本已弃用,请使用v1.0或更高版本")]
public async Task<ActionResult> GetUsers()
{
return Ok(new
{
message = "此API版本已弃用,请升级到v1.0或更高版本",
deprecatedSince = "2024-01-01",
supportEndDate = "2024-06-01"
});
}
}
单元测试与集成测试
测试是确保API质量和稳定性的重要手段。在ASP.NET Core中,我们可以通过多种方式进行API测试。
API测试策略 单元测试 集成测试 端到端测试 控制器测试 业务逻辑测试 数据访问测试 API端点测试 中间件测试 认证授权测试 完整用户流程 第三方集成 性能测试
测试项目配置
首先创建测试项目并安装必要的依赖:
bash
# 创建测试项目
dotnet new xunit -n ApiDocumentationExample.Tests
# 安装测试相关包
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Moq
dotnet add package FluentAssertions
dotnet add package Bogus
dotnet add package WebMotions.Fake.Authentication.JwtBearer
# 添加项目引用
dotnet add reference ../ApiDocumentationExample/ApiDocumentationExample.csproj
基础测试配置类
csharp
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace ApiDocumentationExample.Tests;
/// <summary>
/// 自定义Web应用程序工厂,用于集成测试
/// </summary>
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// 移除真实的数据库上下文
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
// 添加内存数据库用于测试
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("TestDatabase");
});
// 配置测试日志
services.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Warning);
});
// 注册测试专用的服务
services.AddScoped<ITestDataSeeder, TestDataSeeder>();
});
builder.UseEnvironment("Testing");
}
}
/// <summary>
/// 测试基类,提供通用的测试设置
/// </summary>
public abstract class IntegrationTestBase : IClassFixture<CustomWebApplicationFactory<Program>>
{
protected readonly CustomWebApplicationFactory<Program> _factory;
protected readonly HttpClient _client;
protected readonly IServiceScope _scope;
protected readonly ApplicationDbContext _context;
protected IntegrationTestBase(CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient();
_scope = factory.Services.CreateScope();
_context = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// 确保数据库被创建
_context.Database.EnsureCreated();
}
/// <summary>
/// 清理测试数据
/// </summary>
protected virtual async Task CleanupAsync()
{
_context.Users.RemoveRange(_context.Users);
await _context.SaveChangesAsync();
}
/// <summary>
/// 种子测试数据
/// </summary>
protected virtual async Task SeedDataAsync()
{
var seeder = _scope.ServiceProvider.GetRequiredService<ITestDataSeeder>();
await seeder.SeedAsync();
}
public void Dispose()
{
_scope?.Dispose();
_client?.Dispose();
}
}
/// <summary>
/// 测试数据种子接口
/// </summary>
public interface ITestDataSeeder
{
Task SeedAsync();
}
/// <summary>
/// 测试数据种子实现
/// </summary>
public class TestDataSeeder : ITestDataSeeder
{
private readonly ApplicationDbContext _context;
public TestDataSeeder(ApplicationDbContext context)
{
_context = context;
}
public async Task SeedAsync()
{
// 清理现有数据
_context.Users.RemoveRange(_context.Users);
await _context.SaveChangesAsync();
// 添加测试用户
var users = new List<User>
{
new User
{
Id = 1,
Username = "testuser1",
Email = "test1@example.com",
FullName = "测试用户1",
IsActive = true,
CreatedAt = DateTime.Now.AddDays(-30)
},
new User
{
Id = 2,
Username = "testuser2",
Email = "test2@example.com",
FullName = "测试用户2",
IsActive = false,
CreatedAt = DateTime.Now.AddDays(-15)
}
};
_context.Users.AddRange(users);
await _context.SaveChangesAsync();
}
}
控制器单元测试
csharp
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace ApiDocumentationExample.Tests.Controllers;
/// <summary>
/// 用户控制器单元测试
/// </summary>
public class UsersControllerUnitTests
{
private readonly Mock<IUserService> _mockUserService;
private readonly Mock<ILogger<UsersController>> _mockLogger;
private readonly UsersController _controller;
public UsersControllerUnitTests()
{
_mockUserService = new Mock<IUserService>();
_mockLogger = new Mock<ILogger<UsersController>>();
_controller = new UsersController(_mockUserService.Object, _mockLogger.Object);
}
[Fact]
public async Task GetUsers_WithValidParameters_ReturnsOkResult()
{
// Arrange
var expectedUsers = new List<UserDto>
{
new UserDto { Id = 1, Username = "user1", Email = "user1@example.com" },
new UserDto { Id = 2, Username = "user2", Email = "user2@example.com" }
};
var expectedResult = new PagedResult<UserDto>
{
Data = expectedUsers,
Total = 2,
Page = 1,
Size = 10
};
_mockUserService
.Setup(x => x.GetUsersAsync(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>()))
.ReturnsAsync(expectedResult);
// Act
var result = await _controller.GetUsers(1, 10);
// Assert
result.Should().BeOfType<ActionResult<PagedResult<UserDto>>>();
var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
var actualResult = okResult.Value.Should().BeOfType<PagedResult<UserDto>>().Subject;
actualResult.Data.Should().HaveCount(2);
actualResult.Total.Should().Be(2);
actualResult.Page.Should().Be(1);
actualResult.Size.Should().Be(10);
}
[Fact]
public async Task GetUser_WithValidId_ReturnsOkResult()
{
// Arrange
var expectedUser = new UserDto
{
Id = 1,
Username = "testuser",
Email = "test@example.com"
};
_mockUserService
.Setup(x => x.GetUserByIdAsync(1))
.ReturnsAsync(expectedUser);
// Act
var result = await _controller.GetUser(1);
// Assert
result.Should().BeOfType<ActionResult<UserDto>>();
var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
var actualUser = okResult.Value.Should().BeOfType<UserDto>().Subject;
actualUser.Id.Should().Be(1);
actualUser.Username.Should().Be("testuser");
actualUser.Email.Should().Be("test@example.com");
}
[Fact]
public async Task GetUser_WithInvalidId_ReturnsNotFound()
{
// Arrange
_mockUserService
.Setup(x => x.GetUserByIdAsync(999))
.ReturnsAsync((UserDto?)null);
// Act
var result = await _controller.GetUser(999);
// Assert
result.Should().BeOfType<ActionResult<UserDto>>();
result.Result.Should().BeOfType<NotFoundObjectResult>();
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-10)]
public async Task GetUsers_WithInvalidPage_ReturnsBadRequest(int invalidPage)
{
// Act
var result = await _controller.GetUsers(invalidPage, 10);
// Assert
result.Should().BeOfType<ActionResult<PagedResult<UserDto>>>();
result.Result.Should().BeOfType<BadRequestObjectResult>();
}
[Fact]
public async Task CreateUser_WithValidRequest_ReturnsCreatedResult()
{
// Arrange
var createRequest = new CreateUserRequest
{
Username = "newuser",
Email = "newuser@example.com",
FullName = "新用户",
Password = "password123"
};
var expectedUser = new UserDto
{
Id = 3,
Username = "newuser",
Email = "newuser@example.com",
FullName = "新用户"
};
_mockUserService
.Setup(x => x.CreateUserAsync(It.IsAny<CreateUserRequest>()))
.ReturnsAsync(expectedUser);
// Act
var result = await _controller.CreateUser(createRequest);
// Assert
result.Should().BeOfType<ActionResult<UserDto>>();
var createdResult = result.Result.Should().BeOfType<CreatedAtActionResult>().Subject;
var actualUser = createdResult.Value.Should().BeOfType<UserDto>().Subject;
actualUser.Username.Should().Be("newuser");
actualUser.Email.Should().Be("newuser@example.com");
// 验证服务方法被调用
_mockUserService.Verify(x => x.CreateUserAsync(It.IsAny<CreateUserRequest>()), Times.Once);
}
[Fact]
public async Task CreateUser_WhenServiceThrowsException_ReturnsConflict()
{
// Arrange
var createRequest = new CreateUserRequest
{
Username = "existinguser",
Email = "existing@example.com",
FullName = "已存在用户",
Password = "password123"
};
_mockUserService
.Setup(x => x.CreateUserAsync(It.IsAny<CreateUserRequest>()))
.ThrowsAsync(new InvalidOperationException("用户名已存在"));
// Act
var result = await _controller.CreateUser(createRequest);
// Assert
result.Should().BeOfType<ActionResult<UserDto>>();
result.Result.Should().BeOfType<ConflictObjectResult>();
}
}
集成测试示例
csharp
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using System.Net;
using System.Text;
using Xunit;
namespace ApiDocumentationExample.Tests.Integration;
/// <summary>
/// 用户API集成测试
/// </summary>
public class UsersApiIntegrationTests : IntegrationTestBase
{
public UsersApiIntegrationTests(CustomWebApplicationFactory<Program> factory)
: base(factory)
{
}
[Fact]
public async Task GetUsers_ReturnsSuccessAndCorrectContentType()
{
// Arrange
await SeedDataAsync();
// Act
var response = await _client.GetAsync("/api/v1/users");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Content.Headers.ContentType?.ToString().Should().Contain("application/json");
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<PagedResult<UserDto>>(content);
result.Should().NotBeNull();
result!.Data.Should().NotBeEmpty();
result.Total.Should().BeGreaterThan(0);
}
[Fact]
public async Task GetUsers_WithPagination_ReturnsCorrectPage()
{
// Arrange
await SeedDataAsync();
var page = 1;
var size = 1;
// Act
var response = await _client.GetAsync($"/api/v1/users?page={page}&size={size}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<PagedResult<UserDto>>(content);
result.Should().NotBeNull();
result!.Data.Should().HaveCount(1);
result.Page.Should().Be(page);
result.Size.Should().Be(size);
}
[Fact]
public async Task GetUser_WithValidId_ReturnsUser()
{
// Arrange
await SeedDataAsync();
var userId = 1;
// Act
var response = await _client.GetAsync($"/api/v1/users/{userId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
var user = JsonConvert.DeserializeObject<UserDto>(content);
user.Should().NotBeNull();
user!.Id.Should().Be(userId);
}
[Fact]
public async Task GetUser_WithInvalidId_ReturnsNotFound()
{
// Arrange
var invalidUserId = 999;
// Act
var response = await _client.GetAsync($"/api/v1/users/{invalidUserId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task CreateUser_WithValidData_ReturnsCreated()
{
// Arrange
var newUser = new CreateUserRequest
{
Username = "integrationtestuser",
Email = "integration@example.com",
FullName = "集成测试用户",
Password = "password123"
};
var json = JsonConvert.SerializeObject(newUser);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/api/v1/users", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var responseContent = await response.Content.ReadAsStringAsync();
var createdUser = JsonConvert.DeserializeObject<UserDto>(responseContent);
createdUser.Should().NotBeNull();
createdUser!.Username.Should().Be(newUser.Username);
createdUser.Email.Should().Be(newUser.Email);
// 验证Location头
response.Headers.Location.Should().NotBeNull();
response.Headers.Location!.ToString().Should().Contain($"/api/v1/users/{createdUser.Id}");
}
[Fact]
public async Task CreateUser_WithInvalidData_ReturnsBadRequest()
{
// Arrange
var invalidUser = new CreateUserRequest
{
Username = "", // 无效:空用户名
Email = "invalid-email", // 无效:邮箱格式错误
FullName = "",
Password = "123" // 无效:密码太短
};
var json = JsonConvert.SerializeObject(invalidUser);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/api/v1/users", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var responseContent = await response.Content.ReadAsStringAsync();
var error = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
error.Should().NotBeNull();
error!.Message.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task ApiVersioning_V1AndV2_ReturnDifferentResponses()
{
// Arrange
await SeedDataAsync();
// Act - 调用v1版本
var v1Response = await _client.GetAsync("/api/v1/users");
var v2Response = await _client.GetAsync("/api/v2/users");
// Assert
v1Response.StatusCode.Should().Be(HttpStatusCode.OK);
v2Response.StatusCode.Should().Be(HttpStatusCode.OK);
var v1Content = await v1Response.Content.ReadAsStringAsync();
var v2Content = await v2Response.Content.ReadAsStringAsync();
// v1和v2应该返回不同的数据结构
v1Content.Should().NotBe(v2Content);
// 验证响应头中的版本信息
v1Response.Headers.Should().ContainKey("X-API-Version");
v2Response.Headers.Should().ContainKey("X-API-Version");
}
protected override async Task DisposeAsync()
{
await CleanupAsync();
await base.DisposeAsync();
}
}
高级测试技巧
使用Bogus生成测试数据
csharp
using Bogus;
namespace ApiDocumentationExample.Tests.Helpers;
/// <summary>
/// 测试数据生成器
/// </summary>
public static class TestDataGenerator
{
private static readonly Faker<User> UserFaker = new Faker<User>("zh_CN")
.RuleFor(u => u.Id, f => f.Random.Int(1, 1000))
.RuleFor(u => u.Username, f => f.Internet.UserName())
.RuleFor(u => u.Email, f => f.Internet.Email())
.RuleFor(u => u.FullName, f => f.Name.FullName())
.RuleFor(u => u.IsActive, f => f.Random.Bool())
.RuleFor(u => u.CreatedAt, f => f.Date.Past(2));
private static readonly Faker<CreateUserRequest> CreateUserRequestFaker = new Faker<CreateUserRequest>("zh_CN")
.RuleFor(u => u.Username, f => f.Internet.UserName())
.RuleFor(u => u.Email, f => f.Internet.Email())
.RuleFor(u => u.FullName, f => f.Name.FullName())
.RuleFor(u => u.Password, f => f.Internet.Password(8, false, "", "Aa1"));
/// <summary>
/// 生成单个用户
/// </summary>
public static User GenerateUser() => UserFaker.Generate();
/// <summary>
/// 生成用户列表
/// </summary>
public static List<User> GenerateUsers(int count) => UserFaker.Generate(count);
/// <summary>
/// 生成创建用户请求
/// </summary>
public static CreateUserRequest GenerateCreateUserRequest() => CreateUserRequestFaker.Generate();
/// <summary>
/// 生成指定用户名的创建请求
/// </summary>
public static CreateUserRequest GenerateCreateUserRequest(string username)
{
var request = CreateUserRequestFaker.Generate();
request.Username = username;
return request;
}
}
性能测试示例
csharp
using System.Diagnostics;
using FluentAssertions;
using Xunit;
namespace ApiDocumentationExample.Tests.Performance;
/// <summary>
/// API性能测试
/// </summary>
public class PerformanceTests : IntegrationTestBase
{
public PerformanceTests(CustomWebApplicationFactory<Program> factory)
: base(factory)
{
}
[Fact]
public async Task GetUsers_PerformanceTest()
{
// Arrange
await SeedDataAsync();
var stopwatch = new Stopwatch();
var requests = 100;
var maxResponseTime = TimeSpan.FromMilliseconds(200);
// Act
stopwatch.Start();
var tasks = Enumerable.Range(0, requests)
.Select(_ => _client.GetAsync("/api/v1/users"))
.ToArray();
var responses = await Task.WhenAll(tasks);
stopwatch.Stop();
// Assert
var averageResponseTime = stopwatch.Elapsed.TotalMilliseconds / requests;
var successfulResponses = responses.Count(r => r.IsSuccessStatusCode);
successfulResponses.Should().Be(requests);
averageResponseTime.Should().BeLessThan(maxResponseTime.TotalMilliseconds);
// 输出性能指标
Console.WriteLine($"总请求数: {requests}");
Console.WriteLine($"总耗时: {stopwatch.Elapsed.TotalMilliseconds:F2}ms");
Console.WriteLine($"平均响应时间: {averageResponseTime:F2}ms");
Console.WriteLine($"成功率: {(double)successfulResponses / requests * 100:F2}%");
}
[Fact]
public async Task CreateUser_ConcurrencyTest()
{
// Arrange
var concurrentUsers = 50;
var users = Enumerable.Range(0, concurrentUsers)
.Select(i => TestDataGenerator.GenerateCreateUserRequest($"concurrent_user_{i}"))
.ToList();
// Act
var stopwatch = Stopwatch.StartNew();
var tasks = users.Select(async user =>
{
var json = JsonConvert.SerializeObject(user);
var content = new StringContent(json, Encoding.UTF8, "application/json");
return await _client.PostAsync("/api/v1/users", content);
});
var responses = await Task.WhenAll(tasks);
stopwatch.Stop();
// Assert
var successfulCreations = responses.Count(r => r.StatusCode == HttpStatusCode.Created);
var conflictResponses = responses.Count(r => r.StatusCode == HttpStatusCode.Conflict);
// 验证大部分请求成功
successfulCreations.Should().BeGreaterThan(concurrentUsers * 0.8);
// 验证没有数据不一致问题
var totalHandled = successfulCreations + conflictResponses;
totalHandled.Should().Be(concurrentUsers);
Console.WriteLine($"并发用户数: {concurrentUsers}");
Console.WriteLine($"成功创建: {successfulCreations}");
Console.WriteLine($"冲突响应: {conflictResponses}");
Console.WriteLine($"总耗时: {stopwatch.Elapsed.TotalMilliseconds:F2}ms");
}
}
认证和授权测试
csharp
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using WebMotions.Fake.Authentication.JwtBearer;
namespace ApiDocumentationExample.Tests.Auth;
/// <summary>
/// 认证授权测试
/// </summary>
public class AuthenticationTests : IntegrationTestBase
{
public AuthenticationTests(CustomWebApplicationFactory<Program> factory)
: base(factory)
{
}
[Fact]
public async Task ProtectedEndpoint_WithoutToken_ReturnsUnauthorized()
{
// Act
var response = await _client.GetAsync("/api/v1/protected");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task ProtectedEndpoint_WithValidToken_ReturnsSuccess()
{
// Arrange
var token = GenerateJwtToken("testuser", "user");
_client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.GetAsync("/api/v1/protected");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task AdminEndpoint_WithUserRole_ReturnsForbidden()
{
// Arrange
var token = GenerateJwtToken("testuser", "user");
_client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.GetAsync("/api/v1/admin");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
}
[Fact]
public async Task AdminEndpoint_WithAdminRole_ReturnsSuccess()
{
// Arrange
var token = GenerateJwtToken("admin", "admin");
_client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.GetAsync("/api/v1/admin");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
private string GenerateJwtToken(string username, string role)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("this-is-a-test-secret-key-for-jwt-token-generation");
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role),
new Claim("sub", username),
new Claim("jti", Guid.NewGuid().ToString())
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
高级测试技巧
API测试自动化流程
开发者 CI/CD管道 测试套件 测试报告 部署环境 提交代码 触发测试 单元测试 集成测试 API文档验证 性能测试 生成测试报告 返回测试结果 自动部署 部署成功通知 发送失败通知 修复并重新提交 alt [测试通过] [测试失败] 开发者 CI/CD管道 测试套件 测试报告 部署环境
契约测试(Contract Testing)
csharp
using Pact.Consumer.Config;
using Pact.Consumer.Dsl;
using Xunit;
namespace ApiDocumentationExample.Tests.Contract;
/// <summary>
/// API契约测试 - 确保API接口的一致性
/// </summary>
public class UserApiContractTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _httpClient;
public UserApiContractTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_httpClient = factory.CreateClient();
}
[Fact]
public async Task GetUser_ShouldMatchContract()
{
// Arrange - 定义期望的契约
var expectedContract = new
{
id = 1,
username = "testuser",
email = "test@example.com",
fullName = "测试用户",
isActive = true,
createdAt = "2024-01-01T00:00:00Z"
};
// Act - 调用实际API
var response = await _httpClient.GetAsync("/api/v1/users/1");
var content = await response.Content.ReadAsStringAsync();
var actualUser = JsonConvert.DeserializeObject<UserDto>(content);
// Assert - 验证契约匹配
response.StatusCode.Should().Be(HttpStatusCode.OK);
actualUser.Should().NotBeNull();
// 验证必要字段存在且类型正确
actualUser!.Id.Should().BePositive();
actualUser.Username.Should().NotBeNullOrEmpty();
actualUser.Email.Should().MatchRegex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$");
actualUser.FullName.Should().NotBeNullOrEmpty();
actualUser.IsActive.Should().Be(actualUser.IsActive); // 布尔类型验证
actualUser.CreatedAt.Should().BeAfter(DateTime.MinValue);
}
[Fact]
public async Task CreateUser_ShouldFollowContractSpecification()
{
// Arrange - 准备符合契约的请求数据
var createRequest = new CreateUserRequest
{
Username = "contracttest",
Email = "contract@example.com",
FullName = "契约测试用户",
Password = "contractPass123"
};
var json = JsonConvert.SerializeObject(createRequest);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _httpClient.PostAsync("/api/v1/users", content);
// Assert - 验证响应契约
response.StatusCode.Should().Be(HttpStatusCode.Created);
// 验证Location头格式
response.Headers.Location.Should().NotBeNull();
response.Headers.Location!.ToString().Should().MatchRegex(@"/api/v1/users/\d+");
// 验证响应体结构
var responseContent = await response.Content.ReadAsStringAsync();
var createdUser = JsonConvert.DeserializeObject<UserDto>(responseContent);
createdUser.Should().NotBeNull();
createdUser!.Id.Should().BePositive();
createdUser.Username.Should().Be(createRequest.Username);
createdUser.Email.Should().Be(createRequest.Email);
}
}
API文档一致性测试
csharp
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
namespace ApiDocumentationExample.Tests.Documentation;
/// <summary>
/// API文档一致性测试 - 确保实际API与文档描述一致
/// </summary>
public class SwaggerDocumentationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _httpClient;
public SwaggerDocumentationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_httpClient = factory.CreateClient();
}
[Fact]
public async Task SwaggerDocument_ShouldBeValidOpenApiSpec()
{
// Act - 获取Swagger文档
var response = await _httpClient.GetAsync("/swagger/v1/swagger.json");
var swaggerJson = await response.Content.ReadAsStringAsync();
// Assert - 验证文档有效性
response.StatusCode.Should().Be(HttpStatusCode.OK);
swaggerJson.Should().NotBeNullOrEmpty();
// 验证JSON格式
var document = JsonConvert.DeserializeObject(swaggerJson);
document.Should().NotBeNull();
// 验证OpenAPI规范
var openApiDocument = new OpenApiStringReader().Read(swaggerJson, out var diagnostic);
diagnostic.Errors.Should().BeEmpty("Swagger文档应该符合OpenAPI规范");
openApiDocument.Should().NotBeNull();
}
}
负载测试和压力测试
csharp
using NBomber.Contracts;
using NBomber.CSharp;
using NBomber.Http.CSharp;
namespace ApiDocumentationExample.Tests.Load;
/// <summary>
/// API负载测试和压力测试
/// </summary>
public class LoadTests
{
private readonly string _baseUrl = "https://localhost:7001";
[Fact]
public void GetUsers_LoadTest()
{
// 配置负载测试场景
var scenario = Scenario.Create("get_users_load_test", async context =>
{
var response = await HttpClientFactory.Create()
.GetAsync($"{_baseUrl}/api/v1/users?page=1&size=10");
return response.IsSuccessStatusCode ? Response.Ok() : Response.Fail();
})
.WithLoadSimulations(
Simulation.InjectPerSec(rate: 100, during: TimeSpan.FromMinutes(5)), // 每秒100个请求,持续5分钟
Simulation.KeepConstant(copies: 50, during: TimeSpan.FromMinutes(3)) // 保持50个并发用户,持续3分钟
);
// 执行负载测试
var stats = NBomberRunner
.RegisterScenarios(scenario)
.WithReportFolder("load-test-reports")
.WithReportFormats(ReportFormat.Html, ReportFormat.Csv)
.Run();
// 验证性能指标
var okCount = stats.AllOkCount;
var failCount = stats.AllFailCount;
var meanResponseTime = stats.ScenarioStats[0].Ok.Response.Mean;
// 断言性能要求
Assert.True(okCount > 0, "应该有成功的请求");
Assert.True(failCount == 0, "不应该有失败的请求");
Assert.True(meanResponseTime < 500, $"平均响应时间应该小于500ms,实际: {meanResponseTime}ms");
}
}
性能与安全测试
API安全测试
csharp
namespace ApiDocumentationExample.Tests.Security;
/// <summary>
/// API安全测试 - 验证安全防护机制
/// </summary>
public class SecurityTests : IntegrationTestBase
{
public SecurityTests(CustomWebApplicationFactory<Program> factory)
: base(factory)
{
}
[Fact]
public async Task Api_ShouldProtectAgainstSqlInjection()
{
// Arrange - SQL注入攻击载荷
var maliciousInputs = new[]
{
"'; DROP TABLE Users; --",
"1' OR '1'='1",
"admin'/*",
"1; EXEC xp_cmdshell('dir'); --"
};
foreach (var maliciousInput in maliciousInputs)
{
// Act - 尝试在查询参数中注入恶意SQL
var response = await _client.GetAsync($"/api/v1/users?keyword={Uri.EscapeDataString(maliciousInput)}");
// Assert - 应该返回正常响应,不应该导致服务器错误
response.StatusCode.Should().NotBe(HttpStatusCode.InternalServerError,
$"SQL注入攻击不应该导致服务器错误: {maliciousInput}");
}
}
[Fact]
public async Task Api_ShouldProtectAgainstXssAttacks()
{
// Arrange - XSS攻击载荷
var xssPayloads = new[]
{
"<script>alert('XSS')</script>",
"javascript:alert('XSS')",
"<img src=x onerror=alert('XSS')>",
"'\"><script>alert('XSS')</script>"
};
foreach (var payload in xssPayloads)
{
// Act - 尝试创建包含XSS载荷的用户
var userData = new CreateUserRequest
{
Username = "xsstest",
Email = "xss@example.com",
FullName = payload, // XSS载荷
Password = "xssPass123"
};
var json = JsonConvert.SerializeObject(userData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/api/v1/users", content);
if (response.IsSuccessStatusCode)
{
// 获取创建的用户,验证XSS载荷被正确转义
var responseContent = await response.Content.ReadAsStringAsync();
// XSS载荷不应该以原始形式出现在响应中
responseContent.Should().NotContain("<script>", "响应不应包含未转义的脚本标签");
responseContent.Should().NotContain("javascript:", "响应不应包含javascript协议");
}
}
}
[Fact]
public async Task Api_ShouldRateLimitRequests()
{
// Arrange - 快速发送大量请求
var requests = 100;
var tasks = new List<Task<HttpResponseMessage>>();
for (int i = 0; i < requests; i++)
{
tasks.Add(_client.GetAsync("/api/v1/users"));
}
// Act - 并发执行所有请求
var responses = await Task.WhenAll(tasks);
// Assert - 应该有一些请求被限流
var tooManyRequestsCount = responses.Count(r => r.StatusCode == HttpStatusCode.TooManyRequests);
var successCount = responses.Count(r => r.IsSuccessStatusCode);
// 在高并发情况下,应该触发限流
if (requests > 50) // 超过合理阈值时应该有限流
{
(tooManyRequestsCount + successCount).Should().Be(requests);
Console.WriteLine($"成功请求: {successCount}, 被限流请求: {tooManyRequestsCount}");
}
}
}
最佳实践总结
API文档最佳实践
API文档最佳实践 完整性 准确性 可用性 维护性 覆盖所有端点 包含请求/响应示例 错误代码说明 与代码同步 自动化生成 持续验证 交互式界面 搜索功能 代码示例 版本控制 变更日志 弃用策略
测试策略最佳实践
-
测试金字塔原则
- 大量单元测试(快速、可靠)
- 适量集成测试(中等复杂度)
- 少量端到端测试(慢速、复杂)
-
持续集成
- 每次提交都运行测试
- 快速反馈循环
- 自动化部署
-
测试数据管理
- 使用测试专用数据库
- 测试间数据隔离
- 可重复的测试环境
CI/CD集成配置
yaml
# GitHub Actions 工作流示例
name: API Tests and Documentation
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Run Unit Tests
run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"
- name: Run Integration Tests
run: dotnet test --no-build --verbosity normal --filter "Category=Integration"
- name: Generate Swagger Documentation
run: |
dotnet run --project ApiDocumentationExample &
sleep 10
curl -o swagger.json http://localhost:5000/swagger/v1/swagger.json
- name: Validate API Documentation
run: |
# 使用swagger-codegen验证文档
docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli validate -i /local/swagger.json
- name: Upload Test Results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: TestResults/
- name: Upload Coverage Reports
uses: codecov/codecov-action@v3
with:
file: TestResults/*/coverage.cobertura.xml
学习资源与工具推荐
官方文档与学习资源
-
Microsoft官方文档
-
测试相关资源
-
工具和库
- Swashbuckle.AspNetCore
- Asp.Versioning
- NBomber - 负载测试
- Bogus - 测试数据生成
社区资源
-
开源项目
-
博客和教程
总结
本文深入探讨了ASP.NET Core中API文档与测试的最佳实践,从基础的Swagger集成到高级的安全测试,涵盖了完整的开发流程。关键要点包括:
核心要点回顾
- 文档先行:良好的API文档是团队协作和外部集成的基础
- 版本控制:合理的版本策略确保API的平滑演进
- 全面测试:单元测试、集成测试、性能测试缺一不可
- 安全防护:安全测试是保障系统稳定的重要环节
- 自动化:CI/CD集成提高开发效率和质量
实施建议
- 从项目开始就建立完善的文档和测试体系
- 定期审查和更新API文档,确保与实现同步
- 建立测试数据管理策略,保证测试的可重复性
- 投资于自动化工具,减少人工维护成本
- 关注安全测试,建立完善的安全防护机制
通过遵循这些最佳实践,您可以构建出既易于使用又易于维护的高质量API服务。
