ASP.NET Core API文档与测试实战指南

前言

在现代软件开发中,API(应用程序编程接口)已成为不同服务和应用程序之间通信的桥梁。一个优秀的API不仅需要具备良好的功能性,更需要有完善的文档和全面的测试策略。本文将深入探讨ASP.NET Core环境下的API文档生成与测试实践,帮助开发者构建更加健壮和易于维护的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文档最佳实践 完整性 准确性 可用性 维护性 覆盖所有端点 包含请求/响应示例 错误代码说明 与代码同步 自动化生成 持续验证 交互式界面 搜索功能 代码示例 版本控制 变更日志 弃用策略

测试策略最佳实践

  1. 测试金字塔原则

    • 大量单元测试(快速、可靠)
    • 适量集成测试(中等复杂度)
    • 少量端到端测试(慢速、复杂)
  2. 持续集成

    • 每次提交都运行测试
    • 快速反馈循环
    • 自动化部署
  3. 测试数据管理

    • 使用测试专用数据库
    • 测试间数据隔离
    • 可重复的测试环境

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

学习资源与工具推荐

官方文档与学习资源

  1. Microsoft官方文档

  2. 测试相关资源

  3. 工具和库

社区资源

  1. 开源项目

  2. 博客和教程

总结

本文深入探讨了ASP.NET Core中API文档与测试的最佳实践,从基础的Swagger集成到高级的安全测试,涵盖了完整的开发流程。关键要点包括:

核心要点回顾

  1. 文档先行:良好的API文档是团队协作和外部集成的基础
  2. 版本控制:合理的版本策略确保API的平滑演进
  3. 全面测试:单元测试、集成测试、性能测试缺一不可
  4. 安全防护:安全测试是保障系统稳定的重要环节
  5. 自动化:CI/CD集成提高开发效率和质量

实施建议

  • 从项目开始就建立完善的文档和测试体系
  • 定期审查和更新API文档,确保与实现同步
  • 建立测试数据管理策略,保证测试的可重复性
  • 投资于自动化工具,减少人工维护成本
  • 关注安全测试,建立完善的安全防护机制

通过遵循这些最佳实践,您可以构建出既易于使用又易于维护的高质量API服务。


相关推荐
小杰来搬砖2 分钟前
讲解HTTP 状态码
后端
寻月隐君4 分钟前
告别竞态条件:基于 Axum 和 Serde 的 Rust 并发状态管理最佳实践
后端·rust·github
这里有鱼汤5 分钟前
90%的人都会搞错的XGBoost预测逻辑,未来到底怎么预测才对?
后端·机器学习
小杰来搬砖8 分钟前
接口路径规范
后端
David爱编程8 分钟前
Java 的数据类型为什么分为基本类型和引用类型?
java·后端
小杰来搬砖8 分钟前
讲解Java中的@Override
后端
白仑色9 分钟前
Spring Boot 性能优化与最佳实践
spring boot·后端·性能优化·数据库层优化·jvm 层优化·日志优化·transactional优化
是2的10次方啊9 分钟前
🔄 Bean属性转换框架深度对比:从BeanUtils到MapStruct的演进之路
java·后端
hjs_deeplearning42 分钟前
认知篇#10:何为分布式与多智能体?二者联系?
人工智能·分布式·深度学习·学习·agent·智能体
ChinaRainbowSea1 小时前
9-2 MySQL 分析查询语句:EXPLAIN(详细说明)
java·数据库·后端·sql·mysql