0
点赞
收藏
分享

微信扫一扫

Spring MVC API文档与自动化测试教程


目录

  1. Swagger/OpenAPI集成
  2. API文档生成与管理
  3. API First开发实践
  4. 自动生成客户端代码
  5. API契约测试
  6. MockMvc集成测试
  7. API性能测试
  8. API监控与告警
  9. 文档版本管理
  10. 最佳实践
  11. 总结

Swagger/OpenAPI集成

Swagger配置

SpringDoc OpenAPI配置

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.15</version>
</dependency>

OpenAPI配置类

@Configuration
@EnableOpenApi
public class OpenApiConfig {
    
    @Bean
    public OpenAPI customOpenAPI(@Value("${spring.application.version:1.0.0}") String appVersion,
                                 @Value("${spring.application.name:Spring MVC App}") String appName) {
        
        return new OpenAPI()
            .info(new Info()
                .title(appName + " API")
                .version(appVersion)
                .description("基于Spring MVC的企业级应用API文档")
                .contact(new Contact()
                    .name("开发团队")
                    .email("dev@company.com")
                    .url("https://company.com"))
                .license(new License()
                    .name("MIT License")
                    .url("https://opensource.org/licenses/MIT"))
                .termsOfService("https://company.com/terms"))
            .components(new Components()
                .addSecuritySchemes("bearer-jwt", new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT")
                    .in(SecurityScheme.In.HEADER)
                    .name("Authorization"))
                .addSchemas("Error", new Schema<>()
                    .type("object")
                    .addProperty("timestamp", new Schema<>().type("string").format("date-time"))
                    .addProperty("status", new Schema<>().type("integer"))
                    .addProperty("error", new Schema<>().type("string"))
                    .addProperty("message", new Schema<>().type("string"))
                    .addProperty("path", new Schema<>().type("string")))
                .addSchemas("ApiResponse", new Schema<>()
                    .type("object")
                    .addProperty("success", new Schema<>().type("boolean"))
                    .addProperty("message", new Schema<>().type("string"))
                    .addProperty("data", new Schema<>().type("object"))
                    .addProperty("timestamp", new Schema<>().type("string").format("date-time"))))
            .addSecurityItem(new SecurityRequirement().addList("bearer-jwt"))
            .externalDocs(new ExternalDocumentation()
                .description("完整文档")
                .url("https://docs.company.com"));
    }
    
    @Bean
    public OpenApiCustomizer openApiCustomiser() {
        return openApi -> {
            // 添加全局响应
            openApi.getPaths().values().forEach(pathItem -> {
                pathItem.readOperations().forEach(operation -> {
                    operation.addTagsItem("API");
                    
                    // 添加通用响应
                    operation.getResponses().addApiResponse(
                        "400",
                        new ApiResponse()
                            .description("请求参数错误")
                            .content(new MediaType()
                                .schema(new Schema<>().$ref("#/components/schemas/Error"))));
                    
                    operation.getResponses().addApiResponse(
                        "401",
                        new ApiResponse()
                            .description("未授权访问")
                            .content(new MediaType()
                                .schema(new Schema<>().$ref("#/components/schemas/Error"))));
                    
                    operation.getResponses().addApiResponse(
                        "500",
                        new ApiResponse()
                            .description("服务器内部错误")
                            .content(new MediaType()
                                .schema(new Schema<>().$ref("#/components/schemas/Error"))));
                });
            });
        };
    }
}

控制器文档注解

用户管理API文档

@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "用户管理", description = "用户相关操作API")
@SecurityRequirement(name = "bearer-jwt")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @Operation(
        summary = "获取用户列表",
        description = "分页获取系统中的所有用户,支持搜索和过滤",
        tags = {"用户管理"}
    )
    @ApiResponses(value = {
        @ApiResponse(
            responseCode = "200",
            description = "成功获取用户列表",
            content = @Content(
                mediaType = "application/json",
                schema = @Schema(implementation = UserResponseDto.class),
                examples = @Example(
                    summary = "用户列表示例",
                    description = "包含用户基本信息和分页信息的响应",
                    value = """
                    {
                        "content": [
                            {
                                "id": 1,
                                "username": "john.doe",
                                "email": "john.doe@example.com",
                                "firstName": "John",
                                "lastName": "Doe",
                                "status": "ACTIVE",
                                "role": "USER",
                                "createdDate": "2024-01-15T10:30:00Z"
                            }
                        ],
                        "totalElements": 100,
                        "totalPages": 10,
                        "size": 10,
                        "number": 0,
                        "first": true,
                        "last": false
                    }
                    """
                )
            )
        ),
        @ApiResponse(responseCode = "401", description = "未授权访问"),
        @ApiResponse(responseCode = "403", description = "权限不足")
    })
    @GetMapping
    public ResponseEntity<PageResult<UserResponseDto>> getUsers(
        @Parameter(
            description = "页码,从0开始",
            example = "0",
            required = false
        )
        @RequestParam(defaultValue = "0") 
        @Min(0) int page,
        
        @Parameter(
            description = "每页数量",
            example = "20",
            required = false
        )
        @RequestParam(defaultValue = "20") 
        @Min(1) @Max(100) int size,
        
        @Parameter(
            description = "搜索关键词,支持用户名、邮箱搜索",
            example = "john",
            required = false
        )
        @RequestParam(required = false) String search,
        
        @Parameter(
            description = "用户状态过滤",
            example = "ACTIVE",
            required = false
        )
        @RequestParam(required = false) UserStatus status,
        
        @Parameter(
            description = "排序字段",
            example = "createdDate",
            required = false
        )
        @RequestParam(defaultValue = "createdDate") String sortBy,
        
        @Parameter(
            description = "排序方向",
            example = "desc",
            required = false
        )
        @RequestParam(defaultValue = "desc") Sort.Direction sortDir
    ) {
        Pageable pageable = PageRequest.of(page, size, Sort.by(sortDir, sortBy));
        PageResult<User> users = userService.searchUsers(search, status, pageable);
        PageResult<UserResponseDto> response = userMapper.toResponsePages(users);
        
        return ResponseEntity.ok(response);
    }
    
    @Operation(
        summary = "根据ID获取用户详情",
        description = "通过用户ID获取用户的详细信息",
        tags = {"用户管理"}
    )
    @ApiResponses(value = {
        @ApiResponse(
            responseCode = "200",
            description = "成功获取用户详情",
            content = @Content(
                mediaType = "application/json",
                schema = @Schema(implementation = UserDetailDto.class)
            )
        ),
        @ApiResponse(responseCode = "404", description = "用户不存在"),
        @ApiResponse(responseCode = "403", description = "无权限访问")
    })
    @GetMapping("/{id}")
    public ResponseEntity<UserDetailDto> getUserById(
        @Parameter(
            description = "用户ID",
            example = "1",
            required = true
        )
        @PathVariable 
        @NotNull Long id
    ) {
        User user = userService.findById(id);
        UserDetailDto response = userMapper.toDetailDto(user);
        return ResponseEntity.ok(response);
    }
    
    @Operation(
        summary = "创建新用户",
        description = "管理员创建新用户账号",
        tags = {"用户管理"}
    )
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<UserResponseDto> createUser(
        @Parameter(description = "用户创建请求", required = true)
        @Valid @RequestBody UserCreateRequestDto request
    ) {
        User user = userMapper.toEntity(request);
        User savedUser = userService.createUser(user);
        UserResponseDto response = userMapper.toResponseDto(savedUser);
        
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
    
    @Operation(
        summary = "更新用户信息",
        description = "更新用户的基本信息和设置",
        tags = {"用户管理"}
    )
    @PutMapping("/{id}")
    public ResponseEntity<UserResponseDto> updateUser(
        @Parameter(description = "用户ID", required = true)
        @PathVariable @NotNull Long id,
        
        @Parameter(description = "用户更新请求", required = true)
        @Valid @RequestBody UserUpdateRequestDto request
    ) {
        User user = userService.findById(id);
        userMapper.updateEntity(user, request);
        User updatedUser = userService.updateUser(user);
        UserResponseDto response = userMapper.toResponseDto(updatedUser);
        
        return ResponseEntity.ok(response);
    }
    
    @Operation(
        summary = "删除用户",
        description = "软删除用户账号",
        tags = {"用户管理"}
    )
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Void> deleteUser(
        @Parameter(description = "用户ID", required = true)
        @PathVariable @NotNull Long id
    ) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

DTO文档注解

数据传输对象文档

@Schema(description = "用户响应DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserResponseDto {
    
    @Schema(description = "用户ID", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
    private Long id;
    
    @Schema(description = "用户名", example = "john.doe", required = true)
    private String username;
    
    @Schema(description = "邮箱地址", example = "john.doe@example.com", required = true)
    private String email;
    
    @Schema(description = "名字", example = "John")
    private String firstName;
    
    @Schema(description = "姓氏", example = "Doe")
    private String lastName;
    
    @Schema(description = "用户状态", example = "ACTIVE", implementation = UserStatus.class)
    private UserStatus status;
    
    @Schema(description = "用户角色", example = "USER", implementation = UserRole.class)
    private UserRole role;
    
    @Schema(description = "头像URL", example = "https://example.com/avatar.jpg")
    private String avatarUrl;
    
    @Schema(description = "创建时间", example = "2024-01-15T10:30:00Z", accessMode = Schema.AccessMode.READ_ONLY)
    private LocalDateTime createdDate;
    
    @Schema(description = "最后登录时间", example = "2024-01-15T14:20:00Z", accessMode = Schema.AccessMode.READ_ONLY)
    private LocalDateTime lastLogin;
}

@Schema(description = "用户创建请求DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserCreateRequestDto {
    
    @Schema(description = "用户名", example = "john.doe", required = true, minLength = 3, maxLength = 50)
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 50, message = "用户名长度必须在3-50字符之间")
    @Pattern(regexp = "^[a-zA-Z0-9._-]+$", message = "用户名只能包含字母、数字、点、下划线和连字符")
    private String username;
    
    @Schema(description = "邮箱地址", example = "john.doe@example.com", required = true)
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @Schema(description = "密码", example = "SecurePassword123!", required = true, minLength = 8)
    @NotBlank(message = "密码不能为空")
    @Size(min = 8, message = "密码至少8个字符")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&].{8,}$",
             message = "密码必须包含大小写字母、数字和特殊字符")
    private String password;
    
    @Schema(description = "名字", example = "John", maxLength = 50)
    @Size(max = 50, message = "名字不能超过50个字符")
    private String firstName;
    
    @Schema(description = "姓氏", example = "Doe", maxLength = 50)
    @Size(max = 50, message = "用户名不能超过50个字符")
    private String lastName;
    
    @Schema(description = "电话号码", example = "+86 138 0013 8000")
    @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "电话号码格式不正确")
    private String phone;
    
    @Schema(description = "用户角色", example = "USER")
    @NotNull(message = "用户角色不能为空")
    private UserRole role;
    
    @Schema(description = "用户状态", example = "ACTIVE")
    private UserStatus status = UserStatus.ACTIVE;
}

API文档生成与管理

自动文档生成

配置类文档管理

@Component
public class DocumentGenerator {
    
    @Autowired
    private OpenAPIService openAPIService;
    
    @EventListener
    @Async
    public void handleApplicationReady(ApplicationReadyEvent event) {
        generateApiDocumentation();
        generateClientSdks();
    }
    
    private void generateApiDocumentation() {
        try {
            // 生成OpenAPI规范文档
            String openapiSpec = openAPIService.getOpenApiSpec();
            
            // 保存到文件
            Path docsPath = Paths.get("docs/openapi.json");
            Files.createDirectories(docsPath.getParent());
            Files.write(docsPath, openapiSpec.getBytes());
            
            // 生成多种格式文档
            generateMarkdownDocs(openapiSpec);
            generateYamlDocs(openapiSpec);
            generateHtmlDocs(openapiSpec);
            
            logger.info("API文档生成完成: docs/");
            
        } catch (Exception e) {
            logger.error("API文档生成失败", e);
        }
    }
    
    private void generateMarkdownDocs(String openapiSpec) throws IOException {
        OpenAPI openAPI = new OpenAPIParser()
            .readContents(openapiSpec, null, new ParseOptions())
            .getOpenAPI();
        
        MarkdownGenerator generator = new MarkdownGenerator();
        String markdownDocs = generator.generate(openAPI);
        
        Files.write(Paths.get("docs/api.md"), markdownDocs.getBytes());
    }
    
    private void generateYamlDocs(String openapiSpec) throws IOException {
        OpenAPI openAPI = Json.mapper().readValue(openapiSpec, OpenAPI.class);
        String yamlSpec = Yaml.mapper().writeValueAsString(openAPI);
        
        Files.write(Paths.get("docs/openapi.yaml"), yamlSpec.getBytes());
    }
}

文档验证

API规范验证器

@Component
public class ApiSpecValidator {
    
    private final Validator validator;
    
    public ApiSpecValidator() {
        this.validator = Validation.buildDefaultValidatorFactory().getValidator();
    }
    
    @EventListener
    public void validateApiSpec(ApplicationReadyEvent event) {
        try {
            OpenAPI openAPI = openAPIService.getOpenAPI();
            validateOpenAPISpec(openAPI);
            log.info("API规范验证通过");
            
        } catch (Exception e) {
            log.error("API规范验证失败", e);
            throw new IllegalStateException("API规范不符合要求", e);
        }
    }
    
    private void validateOpenAPISpec(OpenAPI openAPI) {
        List<String> errors = new ArrayList<>();
        
        // 验证必需字段
        if (openAPI.getInfo() == null) {
            errors.add("缺少API信息");
        }
        
        if (openAPI.getPaths() == null || openAPI.getPaths().isEmpty()) {
            errors.add("缺少API路径定义");
        }
        
        // 验证每个端点
        openAPI.getPaths().values().forEach(pathItem -> {
            validatePathItem(pathItem, errors);
        });
        
        // 验证组件
        if (openAPI.getComponents() != null) {
            validateComponents(openAPI.getComponents(), errors);
        }
        
        if (!errors.isEmpty()) {
            throw new ValidationException("API规范验证失败: " + String.join(", ", errors));
        }
    }
    
    private void validatePathItem(PathItem pathItem, List<String> errors) {
        List<Operation> operations = Arrays.asList(
            pathItem.getGet(), pathItem.getPost(), pathItem.getPut(), 
            pathItem.getDelete(), pathItem.getPatch(), pathItem.getHead(), pathItem.getOptions()
        );
        
        operations.stream()
            .filter(Objects::nonNull)
            .forEach(operation -> {
                validateOperation(operation, errors);
            });
    }
    
    private void validateOperation(Operation operation, List<String> errors) {
        if (operation.getOperationId() == null || operation.getOperationId().isEmpty()) {
            errors.add("操作缺少operationId");
        }
        
        if (operation.getResponses() == null || operation.getResponses().isEmpty()) {
            errors.add("操作 " + operation.getOperationId() + " 缺少响应定义");
        }
        
        // 验证必须有2xx响应
        boolean hasSuccessResponse = operation.getResponses().keySet().stream()
            .anyMatch(code -> code.startsWith("2"));
        
        if (!hasSuccessResponse) {
            errors.add("操作 " + operation.getOperationId() + " 缺少成功响应");
        }
    }
}

API First开发实践

契约优先开发

API契约定义

# contract/user-api.yaml
openapi: 3.0.3
info:
  title: User Management API
  version: 1.0.0
  description: 用户管理相关API
servers:
  - url: http://localhost:8080/api/v1
    description: 开发环境
  - url: https://api.company.com/api/v1
    description: 生产环境
components:
  schemas:
    User:
      type: object
      required:
        - id
        - username
        - email
        - status
      properties:
        id:
          type: integer
          format: int64
          description: 用户ID
        username:
          type: string
          description: 用户名
          example: "john.doe"
        email:
          type: string
          format: email
          description: 邮箱地址
          example: "john.doe@example.com"
        firstName:
          type: string
          description: 名字
          example: "John"
        lastName:
          type: string
          description: 姓氏
          example: "Doe"
        status:
          type: string
          enum: [ACTIVE, INACTIVE, SUSPENDED]
          description: 用户状态
        createdDate:
          type: string
          format: date-time
          description: 创建时间
    
    UserCreateRequest:
      type: object
      required:
        - username
        - email
        - password
      properties:
        username:
          type: string
          pattern: '^[a-zA-Z0-9._-]+$'
          minLength: 3
          maxLength: 50
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
        firstName:
          type: string
          maxLength: 50
        lastName:
          type: string
          maxLength: 50
        phone:
          type: string
          pattern: '^\\+?[1-9]\\d{1,14}$'
  
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

paths:
  /users:
    get:
      summary: 获取用户列表
      description: 分页获取用户列表,支持搜索和过滤
      security:
        - bearerAuth: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
          description: 页码
        - name: size
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: 每页数量
        - name: search
          in: query
          schema:
            type: string
          description: 搜索关键词
        - name: status
          in: query
          schema:
            type: string
            enum: [ACTIVE, INACTIVE, SUSPENDED]
          description: 用户状态过滤
      responses:
        '200':
          description: 成功获取用户列表
          content:
            application/json:
              schema:
                type: object
                properties:
                  content:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  totalElements:
                    type: integer
                  totalPages:
                    type: integer
                  size:
                    type: integer
                  number:
                    type: integer
    
    post:
      summary: 创建新用户
      description: 创建新的用户账号
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserCreateRequest'
      responses:
        '201':
          description: 用户创建成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

契约测试

API契约测试实现

@TestComponent
public class ContractTestProvider {
    
    @PactTestFor(pacticipant = "user-service")
    public void getUserList(PactDslWithProvider builder) {
        
        PactDslJsonArray body = new PactDslJsonArray()
            .object()
            .numberValue("id", 1)
            .stringType("username", "john.doe")
            .stringType("email", "john.doe@example.com")
            .stringType("status", "ACTIVE")
            .closeObject();
        
        builder
            .given("用户列表存在")
            .uponReceiving("获取用户列表请求")
            .path("/api/v1/users")
            .method("GET")
            .headers(Map.of("Authorization", "Bearer token"))
            .willRespondWith()
            .status(200)
            .matchHeader("Content-Type", "application/json")
            .body(body);
    }
    
    @PactTestFor(pacticipant = "user-service")
    public void createUser(PactDslWithProvider builder) {
        
        PactDslJsonBody requestBody = new PactDslJsonBody()
            .stringType("username", "test.user")
            .stringType("email", "test@example.com")
            .stringType("password", "password123");
        
        PactDslJsonBody responseBody = new PactDslJsonBody()
            .numberType("id", 1)
            .stringType("username", "test.user")
            .stringType("email", "test@example.com")
            .stringType("status", "ACTIVE");
        
        builder
            .given("系统可以创建新用户")
            .uponReceiving("创建用户请求")
            .path("/api/v1/users")
            .method("POST")
            .headers(Map.of(
                "Content-Type", "application/json",
                "Authorization", "Bearer token"
            ))
            .body(requestBody)
            .willRespondWith()
            .status(201)
            .matchHeader("Content-Type", "application/json")
            .body(responseBody);
    }
}

@RunWith(PactRunner.class)
@Provider("user-service")
@PactFolder("pacts")
public class UserClientPactTest {
    
    @Rule
    public final PactProviderRuleMk2 providerRule = new PactProviderRuleMk2("user-service", null, 8080, this);
    
    @InjectMocks
    private UserApiClient userApiClient;
    
    @Before
    public void setUp() {
        RestTemplate restTemplate = new RestTemplate();
        userApiClient = new UserApiClientImpl("http://localhost:8080", restTemplate);
    }
    
    @Test
    @PactVerification(value = "user-service", fragment = "getUserList")
    public void testGetUserList() {
        List<User> users = userApiClient.getUsers();
        
        assertThat(users).isNotEmpty();
        assertThat(users.get(0).getUsername()).isEqualTo("john.doe");
        assertThat(users.get(0).getEmail()).isEqualTo("john.doe@example.com");
    }
    
    @Test
    @PactVerification(value = "user-service", fragment = "createUser")
    public void testCreateUser() {
        UserCreateRequest request = UserCreateRequest.builder()
            .username("test.user")
            .email("test@example.com")
            .password("password123")
            .build();
        
        User user = userApiClient.createUser(request);
        
        assertThat(user.getUsername()).isEqualTo("test.user");
        assertThat(user.getEmail()).isEqualTo("test@example.com");
        assertThat(user.getStatus()).isEqualTo(UserStatus.ACTIVE);
    }
}

MockMvc集成测试

Controller测试

REST控制器测试

@WebMvcTest(UserController.class)
@Import({SecurityConfig.class, TestConfig.class})
@TestPropertySource(properties = {
    "spring.jpa.hibernate.ddl-auto=none",
    "logging.level.org.springframework.web=debug"
})
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @MockBean
    private UserMapper userMapper;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    @WithMockUser(roles = "ADMIN")
    @DisplayName("获取用户列表 - 成功")
    void testGetUsers_Success() throws Exception {
        // 准备测试数据
        User user1 = User.builder()
            .id(1L)
            .username("john.doe")
            .email("john@example.com")
            .firstName("John")
            .lastName("Doe")
            .status(UserStatus.ACTIVE)
            .role(UserRole.USER)
            .createdDate(LocalDateTime.now())
            .build();
        
        PageResult<User> mockPageResult = new PageResult<>(
            Lists.newArrayList(user1),
            0, 20, 1, 1L, true, false
        );
        
        UserResponseDto responseDto = UserResponseDto.builder()
            .id(1L)
            .username("john.doe")
            .email("john@example.com")
            .firstName("John")
            .lastName("Doe")
            .status(UserStatus.ACTIVE)
            .role(UserRole.USER)
            .build();
        
        PageResult<UserResponseDto> expectedResponse = new PageResult<>(
            Lists.newArrayList(responseDto),
            0, 20, 1, 1L, true, false
        );
        
        // Mock服务层
        when(userService.searchUsers(anyString(), any(), any(Pageable.class)))
            .thenReturn(mockPageResult);
        when(userMapper.toResponsePages(any()))
            .thenReturn(expectedResponse);
        
        // 执行测试
        mockMvc.perform(get("/api/v1/users")
                .param("page", "0")
                .param("size", "20")
                .param("search", "john")
                .param("status", "ACTIVE")
                .param("sortBy", "createdDate")
                .param("sortDir", "desc")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.content").isArray())
            .andExpect(jsonPath("$.content.length()").value(1))
            .andExpect(jsonPath("$.content[0].id").value(1))
            .andExpect(jsonPath("$.content[0].username").value("john.doe"))
            .andExpect(jsonPath("$.content[0].email").value("john@example.com"))
            .andExpect(jsonPath("$.totalElements").value(1))
            .andExpect(jsonPath("$.totalPages").value(1))
            .andDo(document("get-users",
                requestParameters(
                    parameterWithName("page").description("页码"),
                    parameterWithName("size").description("每页数量"),
                    parameterWithName("search").description("搜索关键词"),
                    parameterWithName("status").description("用户状态"),
                    parameterWithName("sortBy").description("排序字段"),
                    parameterWithName("sortDir").description("排序方向")
                ),
                responseFields(
                    fieldWithPath("content").description("用户列表"),
                    fieldWithPath("content[].id").description("用户ID"),
                    fieldWithPath("content[].username").description("用户名"),
                    fieldWithPath("content[].email").description("邮箱"),
                    fieldWithPath("totalElements").description("总记录数"),
                    fieldWithPath("totalPages").description("总页数"),
                    fieldWithPath("number").description("当前页"),
                    fieldWithPath("size").description("页面大小")
                )
            ));
        
        // 验证服务调用
        verify(userService).searchUsers("john", UserStatus.ACTIVE, any(Pageable.class));
        verify(userMapper).toResponsePages(mockPageResult);
    }
    
    @Test
    @WithMockUser(roles = "ADMIN")
    @DisplayName("创建用户 - 成功")
    void testCreateUser_Success() throws Exception {
        // 准备测试数据
        UserCreateRequestDto requestDto = UserCreateRequestDto.builder()
            .username("test.user")
            .email("test@example.com")
            .password("password123")
            .firstName("Test")
            .lastName("User")
            .role(UserRole.USER)
            .status(UserStatus.ACTIVE)
            .build();
        
        User newUser = User.builder()
            .id(1L)
            .username("test.user")
            .email("test@example.com")
            .firstName("Test")
            .lastName("User")
            .role(UserRole.USER)
            .status(UserStatus.ACTIVE)
            .build();
        
        UserResponseDto responseDto = UserResponseDto.builder()
            .id(1L)
            .username("test.user")
            .email("test@example.com")
            .firstName("Test")
            .lastName("User")
            .status(UserStatus.ACTIVE)
            .role(UserRole.USER)
            .build();
        
        // Mock服务层
        when(userMapper.toEntity(any(UserCreateRequestDto.class)))
            .thenReturn(newUser);
        when(userService.createUser(any(User.class)))
            .thenReturn(newUser);
        when(userMapper.toResponseDto(any(User.class)))
            .thenReturn(responseDto);
        
        // 执行测试
        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(requestDto)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.username").value("test.user"))
            .andExpect(jsonPath("$.email").value("test@example.com"))
            .andDo(document("create-user",
                requestFields(
                    fieldWithPath("username").description("用户名"),
                    fieldWithPath("email").description("邮箱"),
                    fieldWithPath("password").description("密码"),
                    fieldWithPath("firstName").description("名字"),
                    fieldWithPath("lastName").description("姓氏"),
                    fieldWithPath("role").description("用户角色"),
                    fieldWithPath("status").description("用户状态")
                ),
                responseFields(
                    fieldWithPath("id").description("用户ID"),
                    fieldWithPath("username").description("用户名"),
                    fieldWithPath("email").description("邮箱"),
                    fieldWithPath("firstName").description("名字"),
                    fieldWithPath("lastName").description("姓氏"),
                    fieldWithPath("status").description("用户状态"),
                    fieldWithPath("role").description("用户角色")
                )
            ));
        
        // 验证服务调用
        verify(userService).createUser(any(User.class));
        verify(userMapper).toEntity(requestDto);
        verify(userMapper).toResponseDto(newUser);
    }
    
    @Test
    @WithMockUser(roles = "USER")
    @DisplayName("创建用户 - 权限不足")
    void testCreateUser_Unauthorized() throws Exception {
        UserCreateRequestDto requestDto = UserCreateRequestDto.builder()
            .username("test.user")
            .email("test@example.com")
            .password("password123")
            .build();
        
        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(requestDto)))
            .andExpect(status().isForbidden());
    }
    
    @Test
    @WithAnonymousUser
    @DisplayName("访问受保护端点 - 未认证")
    void testAccessProtectedEndpoint_Unauthenticated() throws Exception {
        mockMvc.perform(get("/api/v1/users"))
            .andExpect(status().isUnauthorized());
    }
    
    @Test
    @WithMockUser(roles = "ADMIN")
    @DisplayName("获取用户详情 - 用户不存在")
    void testGetUserById_NotFound() throws Exception {
        when(userService.findById(999L))
            .thenThrow(new UserNotFoundException("用户不存在"));
        
        mockMvc.perform(get("/api/v1/users/999"))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$message").value("用户不存在"));
    }
}

API性能测试

JMeter测试

JMeter测试计划配置

<jmeterTestPlan version="1.2">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="User API Performance Test" enabled="true">
      <elementProp name="TestPlan.arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="baseUrl" elementType="Argument">
            <stringProp name="Argument.name">baseUrl</stringProp>
            <stringProp name="Argument.value">http://localhost:8080/api/v1</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
      </elementProp>
    </TestPlan>
    
    <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Load Test - Get Users" enabled="true">
      <stringProp name="ThreadGroup.num_threads">100</stringProp>
      <stringProp name="ThreadGroup.ramp_time">30</stringProp>
      <stringProp name="ThreadGroup.duration">300</stringProp>
      <stringProp name="ThreadGroup.delay"></stringProp>
      <boolProp name="ThreadGroup.scheduler">true</boolProp>
      
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Users List" enabled="true">
          <stringProp name="HTTPSampler.domain">${baseUrl}</stringProp>
          <stringProp name="HTTPSampler.port">8080</stringProp>
          <stringProp name="HTTPSampler.protocol">http</stringProp>
          <stringProp name="HTTPSampler.path">/users</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <stringProp name="HTTPSampler.DO_MULTIPART_POST">false</stringProp>
        </HTTPSamplerProxy>
      </hashTree>
    </ThreadGroup>
  </hashTree>
</jmeterTestPlan>

Gatling性能测试

Gatling测试场景

import io.gatling.core.Predef._
import io.gatling.http.Predef._

class UserApiLoadTest extends Simulation {
  
  val httpConf = http
    .baseURL("http://localhost:8080/api/v1")
    .acceptHeader("application/json")
    .contentTypeHeader("application/json")
    .authorizationHeader("Bearer ${token}")
  
  val scn = scenario("User API Load Test")
    .exec(
      http("Get Users List")
        .get("/users")
        .queryParam("page", "0")
        .queryParam("size", "20")
        .check(status.is(200))
        .check(jsonPath("$.content").exists)
        .check(jsonPath("$.totalElements").isNumber)
        .check(jsonPath("$.totalPages").isNumber)
    )
    .pause(1, 3)
    .exec(
      http("Get User by ID")
        .get("/users/1")
        .check(status.is(200))
        .check(jsonPath("$.id").is(1))
        .check(jsonPath("$.username").exists)
        .check(jsonPath("$.email").exists)
    )
    .pause(2, 5)
    .exec(
      http("Create User")
        .post("/users")
        .body(StringBody(
          """
          {
            "username": "loadtest_${random}",
            "email": "loadtest_${random}@example.com",
            "password": "LoadTest123!",
            "firstName": "LoadTest",
            "lastName": "User",
            "role": "USER"
          }
          """.stripMargin
        ))
        .check(status.is(201))
        .check(jsonPath("$.id").saveAs("newUserId"))
        .check(jsonPath("$.username").saveAs("newUsername"))
    )
    .pause(1, 2)
    .exec(
      http("Update User")
        .put("/users/${newUserId}")
        .body(StringBody(
          """
          {
            "firstName": "UpdatedLoadTest",
            "lastName": "UpdatedUser",
            "phone": "+86 138 0013 8000"
          }
          """.stripMargin
        ))
        .check(status.is(200))
        .check(jsonPath("$.firstName").is("UpdatedLoadTest"))
    )
  
  setUp(
    scn.inject(
      nothingFor(5.seconds),
      rampUsers(50) during(30.seconds),
      constantUsersPerSec(20) during(2.minutes),
      nothingFor(10.seconds),
      rampUsers(100) during(30.seconds),
      constantUsersPerSec(50) during(5.minutes)
    )
  ).protocols(httpConf)
   .assertions(
     global.responseTime.percentile3.lessThan(2000),
     global.successfulRequests.percent.greaterThan(95),
     forAll.failedRequests.percent.lessThan(5)
   )
}

总结

Spring MVC API文档与自动化测试教程提供了完整的API开发质量保证方案:

核心技术要点

  1. API文档生成 - Swagger/OpenAPI自动文档工具
  2. 契约优先开发 - API First开发方法论
  3. 自动测试覆盖 - MockMvc单元测试和集成测试
  4. 性能测试 - JMeter和Gatling负载测试
  5. 客户端生成 - 自动生成多语言SDK
  6. 文档管理 - 版本控制和文档发布

企业级应用价值

  • 开发效率双倍提升:API优先开发模式大大提高开发效率
  • 团队协作无缝对接:标准化的API文档确保前后端协作顺畅
  • 测试覆盖率高达90%+:自动化测试确保API质量稳定可靠
  • 性能指标可视化监控:实时掌握API性能和用户体验指标

最佳实践建议

  1. 契约优先:先定义API契约,再进行实现
  2. 自动化覆盖:代码覆盖率要保持在90%以上
  3. 文档实时更新:API文档要与代码实现同步更新
  4. 性能基准设定:建立明确的性能基准和告警机制
  5. 版本管理规范:API变更要遵循语义化版本规范

掌握API文档与测试技术,将使您的Spring MVC应用在企业级开发中更具竞争力和可靠性!


举报

相关推荐

0 条评论