ARTICLE · 1055487
【架构实战】接口文档与契约测试:让协作更高效
【架构实战】接口文档与契约测试:让协作更高效
一、接口文档不一致让我背了锅
2020年,后端说接口返回的status字段是Integer类型,前端按Integer处理的。
但实际返回的是String类型。前端页面显示"支付状态: 1"而不是"支付状态: 已支付"。
用户投诉后,老板问谁的问题。后端说文档写的是Integer,前端说他们按文档开发的。文档和代码不一致,谁也说不清。
最后前端和后端各打50大板。但从那以后,我们引入了契约测试,文档和代码永远保持一致。
二、Swagger/SpringDoc
2.1 配置
/** * SpringDoc配置 */@ConfigurationpublicclassSpringDocConfig {@Beanpublic OpenAPI customOpenAPI() {returnnewOpenAPI() .info(newInfo() .title("电商系统API文档") .version("v2.0") .description("电商系统所有对外API接口文档") .contact(newContact() .name("架构团队") .email("arch@example.com"))) .externalDocs(newExternalDocumentation() .description("架构Wiki") .url("https://wiki.example.com")) .addSecurityItem(newSecurityRequirement().addList("bearerAuth")) .components(newComponents() .addSecuritySchemes("bearerAuth",newSecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))); }@Beanpublic GroupedOpenApi orderApi() {return GroupedOpenApi.builder() .group("订单服务") .pathsToMatch("/api/v2/orders/**") .addOpenApiMethodFilter(method -> method.isAnnotationPresent(RequiresPermission.class)) .build(); }@Beanpublic GroupedOpenApi productApi() {return GroupedOpenApi.builder() .group("商品服务") .pathsToMatch("/api/v2/products/**") .build(); }}2.2 注解使用
/** * 订单API(完整注解示例) */@RestController@RequestMapping("/api/v2/orders")@Tag(name = "订单管理", description = "订单相关API")publicclassOrderApiController {@Operation( summary = "创建订单", description = "用户下单创建订单,需要登录", responses = { @ApiResponse(responseCode = "200", description = "创建成功"), @ApiResponse(responseCode = "400", description = "参数错误"), @ApiResponse(responseCode = "401", description = "未登录"), @ApiResponse(responseCode = "429", description = "请求过于频繁") } )@PostMappingpublic Result<OrderVO> createOrder(@Parameter(description = "创建订单请求", required = true)@RequestBody@Valid CreateOrderRequest request) {Orderorder= orderService.createOrder(request);return Result.success(OrderVO.from(order)); }}/** * 创建订单请求 */@Data@Schema(description = "创建订单请求")publicclassCreateOrderRequest {@Schema(description = "收货地址ID", example = "1001", required = true)@NotNull(message = "收货地址不能为空")private Long addressId;@Schema(description = "优惠券ID", example = "2001")private Long couponId;@Schema(description = "订单商品列表", required = true)@NotEmpty(message = "商品列表不能为空")private List<OrderItemDTO> items;@Schema(description = "备注")@Size(max = 200, message = "备注不能超过200字")private String remark;}/** * 订单VO */@Data@Schema(description = "订单信息")publicclassOrderVO {@Schema(description = "订单ID", example = "ORD202401010001")private String orderId;@Schema(description = "订单状态", example = "UNPAID", allowableValues = {"UNPAID", "PAID", "SHIPPED", "COMPLETED", "CANCELLED"})private String status;@Schema(description = "订单金额", example = "99.90")private BigDecimal amount;}三、契约测试
3.1 Spring Cloud Contract
/** * 契约定义(Groovy DSL) */// file: contracts/order-service/shouldCreateOrder.groovyorg.springframework.cloud.contract.spec.Contract.make { request { method POST() url "/api/v2/orders" headers { contentType(applicationJson()) header("Authorization", "Bearer token") } body( addressId: 1001, couponId: 2001, items: [ [productId: 3001, quantity: 2, price: 49.95] ] ) } response { status 200 headers { contentType(applicationJson()) } body( code: 200, data: [ orderId: value(consumer(regex("ORD[0-9]+")), producer("ORD202401010001")), status: "UNPAID", amount: 99.90 ] ) }}3.2 消费者端测试
/** * 消费者端契约测试 */@SpringBootTest@AutoConfigureStubRunner( ids = "com.example:order-service:+:stubs:8080", stubsMode = StubRunnerProperties.StubsMode.LOCAL)publicclassOrderServiceContractTest {@Autowiredprivate OrderClient orderClient;@TestpublicvoidshouldCreateOrder() {CreateOrderRequestrequest=newCreateOrderRequest(); request.setAddressId(1001L); request.setCouponId(2001L); request.setItems(List.of(newOrderItemDTO(3001L, 2, newBigDecimal("49.95")) )); Result<OrderVO> result = orderClient.createOrder(request); assertThat(result.getCode()).isEqualTo(200); assertThat(result.getData().getStatus()).isEqualTo("UNPAID"); assertThat(result.getData().getAmount()).isEqualByComparingTo("99.90"); }}四、踩坑实录
坑1:文档和代码不同步
代码改了但文档没更新,前端按旧文档开发。
解决:使用SpringDoc自动生成文档,或契约测试强制一致性。
坑2:接口设计不一致
不同的接口风格不统一,有的用驼峰有的用下划线。
解决:制定接口设计规范,Code Review检查。
坑3:没有Mock
前端等后端接口开发完才能联调,效率低。
解决:先定义接口契约,前端用Mock数据开发。
坑4:接口文档没有版本
接口变更后,旧版文档找不到了。
解决:文档跟随代码版本,每个版本有独立文档。
坑5:契约测试维护成本高
契约太多,每次改动要改很多测试。
解决:只对核心接口做契约测试,其他用Swagger文档。
五、总结
接口文档与契约测试要点:
最佳实践:
文档自动生成,不要手写 核心接口做契约测试 接口设计规范统一 版本管理 文档和代码同仓库
血的教训:
接口文档不是附属品,是代码的一部分。文档和代码不同步,比没有文档更可怕。
思考题: 你的团队怎么管理接口文档的?
个人观点,仅供参考