夜雨聆风学习资料网

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(3001L2newBigDecimal("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文档。


五、总结

接口文档与契约测试要点:

工具
用途
SpringDoc
自动生成API文档
Swagger UI
在线测试API
Spring Cloud Contract
契约测试
WireMock
Mock服务

最佳实践:

  1. 文档自动生成,不要手写
  2. 核心接口做契约测试
  3. 接口设计规范统一
  4. 版本管理
  5. 文档和代码同仓库

血的教训:

接口文档不是附属品,是代码的一部分。文档和代码不同步,比没有文档更可怕。

思考题: 你的团队怎么管理接口文档的?


个人观点,仅供参考

相关学习资料