8 种测试方法,让 Spring Boot 接口稳定性直接翻倍!从踩坑到落地全攻略

前阵子线上出了个糟心事:刚上线的商品查询接口,平时测试都好好的,大促一到直接超时,用户刷不出商品列表,订单量掉了一大半。排查后才发现,是没做高并发压测,数据库连接池满了都没察觉 —— 这事儿让我明白,接口稳定性不是 “功能跑通” 就够的,得靠一套完整的测试体系保驾护航。

这篇文章就把我踩过坑后总结的 8 种 Spring Boot 接口测试方法分享出来,从基础的单元测试到极端场景的混沌测试,每一步都附上手把手的实战思路,新手也能跟着做,帮你把接口稳定性从 “靠运气” 变成 “有保障”。

一、单元测试:把接口逻辑的 “小 bug” 掐死在摇篮里

单元测试是最基础也最关键的一步,重点验证接口里的 “核心逻辑”—— 比如参数校验、业务规则判断、异常处理这些细节。很多人觉得 “写单元测试浪费时间”,但其实它能帮你提前发现比如 “库存扣成负数”“价格传成字符串” 这类低级但致命的问题。

怎么落地?

用 JUnit 5+Mockito 就能搞定,不用复杂配置。以 “商品库存扣减” 接口为例,核心是测 3 种场景:库存够、库存刚好、库存不够。

// 用Mockito模拟依赖,不用连真实数据库

@ExtendWith(MockitoExtension.class)

public class ProductStockServiceTest {

    // 模拟库存DAO(不用真的操作数据库)

    @Mock

    private ProductStockDao stockDao;

    // 把模拟的DAO注入到要测试的服务里

    @InjectMocks

    private ProductStockService stockService;

    // 场景1:库存充足,扣减成功

    @Test

    void deductStock_success() {

        // 1. 假设查出来库存有100

        Long productId = 1001L;

        int deductNum = 20;

        when(stockDao.getStockByProductId(productId)).thenReturn(100);

        // 2. 调用扣减方法

        boolean result = stockService.deductStock(productId, deductNum);

        // 3. 验证结果:扣减成功,且库存更新成80

        assertThat(result).isTrue();

        verify(stockDao, times(1)).updateStock(productId, 80);

    }

    // 场景2:库存不够,扣减失败

    @Test

    void deductStock_insufficient() {

        Long productId = 1001L;

        int deductNum = 30;

        // 查出来库存只有20,不够扣

        when(stockDao.getStockByProductId(productId)).thenReturn(20);

        boolean result = stockService.deductStock(productId, deductNum);

        // 验证:扣减失败,且没调用更新库存的方法

        assertThat(result).isFalse();

        verify(stockDao, never()).updateStock(anyLong(), anyInt());

    }

}

新手小贴士:

别测私有方法:单元测试只测对外的 public 方法,私有方法通过调用 public 方法间接覆盖;

不用连真实依赖:数据库、Redis 这些都用 Mockito 模拟,不然测试会受环境影响,时好时坏;

覆盖率不用追求 100%:核心业务逻辑(比如支付、库存)要到 80% 以上,非核心的工具类不用强求。

二、API 测试:确保前后端 / 服务间 “说同一种话”

API 测试主要验证 “接口契约”—— 比如前端传什么参数、后端返回什么格式、状态码对不对。之前跟前端合作时,就因为没做 API 测试,后端把 “productId” 改成了 “id”,前端没改,上线后直接报错,排查半天才发现是字段名不匹配。

怎么落地?

用 Spring Boot 自带的 TestRestTemplate,不用启动 Postman,直接在代码里发 HTTP 请求。以 “商品查询” 接口为例,重点测 “查得到”“查不到”“参数错” 三种情况。

// 启动Spring上下文,模拟Web环境(用随机端口,不占用真实端口)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

public class ProductControllerApiTest {

    // 注入Spring内置的测试工具,用来发HTTP请求

    @Autowired

    private TestRestTemplate restTemplate;

    // 场景1:查存在的商品,返回200和正确数据

    @Test

    void getProductById_success() {

        Long productId = 1001L;

        // 发GET请求:http://localhost:随机端口/api/product/1001

        ResponseEntity<ProductDTO> response = restTemplate.getForEntity(

                "/api/product/" + productId,

                ProductDTO.class

        );

        // 验证:状态码是200,返回的商品名和价格对得上

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        ProductDTO product = response.getBody();

        assertThat(product.getProductName()).isEqualTo("华为Mate 60 Pro");

        assertThat(product.getPrice()).isBetween(6000.0f, 7000.0f);

    }

    // 场景2:查不存在的商品,返回404

    @Test

    void getProductById_notFound() {

        Long productId = 9999L; // 不存在的ID

        ResponseEntity<ErrorDTO> response = restTemplate.getForEntity(

                "/api/product/" + productId,

                ErrorDTO.class

        );

        // 验证:状态码404,错误提示正确

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);

        assertThat(response.getBody().getMessage()).contains("商品不存在");

    }

}

新手小贴士:

统一错误格式:所有接口的错误响应都用同一格式(比如{code: "", message: ""}),测试断言更方便;

状态码要对应:成功用 200,参数错用 400,没权限用 401,找不到资源用 404,别乱返回;

加进 CI 流水线:每次代码提交自动跑 API 测试,不然改了代码没人测,很容易出问题。

三、参数校验测试:别让 “脏数据” 溜进数据库

参数校验没做好,后果能有多严重?之前有个同事写订单接口时,没校验 “购买数量” 不能为负,结果有人恶意传了 “-10”,导致库存变成负数,后续发货时全乱了。参数校验测试就是把这些 “非法参数” 挡在外面。

怎么落地?

用 Spring Validation 的注解(比如@NotNull、@Min)先定义规则,再测 “参数缺了”“参数超范围” 这些场景。

先定义订单创建的 DTO,加校验注解:

// 订单创建参数DTO

public class OrderCreateDTO {

    @NotNull(message = "用户ID不能为空") // 必传

    private Long userId;

    @NotNull(message = "商品ID不能为空")

    private Long productId;

    @Min(value = 1, message = "购买数量不能小于1") // 至少买1件

    @Max(value = 10, message = "购买数量不能超过10") // 最多买10件

    private Integer quantity;

    // getter/setter省略

}

再写测试,故意传非法参数:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

public class OrderControllerValidationTest {

    @Autowired

    private TestRestTemplate restTemplate;

    // 场景1:购买数量为0,校验失败

    @Test

    void createOrder_invalidQuantity() {

        // 1. 构造请求:购买数量传0,违反@Min(1)

        OrderCreateDTO createDTO = new OrderCreateDTO();

        createDTO.setUserId(2001L);

        createDTO.setProductId(1001L);

        createDTO.setQuantity(0); // 非法参数

        // 2. 设置请求头为JSON格式

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity<OrderCreateDTO> request = new HttpEntity<>(createDTO, headers);

        // 3. 发POST请求

        ResponseEntity<ErrorDTO> response = restTemplate.postForEntity(

                "/api/order",

                request,

                ErrorDTO.class

        );

        // 4. 验证:返回400,提示“购买数量不能小于1”

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);

        assertThat(response.getBody().getMessage()).contains("购买数量不能小于1");

    }

}

新手小贴士:

自定义校验规则:比如手机号、身份证号,用@Pattern或者自定义注解,别在业务代码里写校验逻辑;

全局处理校验异常:用@ControllerAdvice统一捕获校验失败的异常,返回标准化的错误响应,不用每个接口都写一遍;

所有参数都要测:别只测必填项,可选参数、边界值(比如数量传 10000)也要测。

四、Mock 测试:不用等依赖服务,自己就能测

做支付接口时最头疼的就是 —— 支付网关还没开发完,我们的回调接口没法测。这时候 Mock 测试就派上用场了:模拟支付网关的响应,不管对方有没有开发好,我们都能正常测自己的接口。

怎么落地?

用 WireMock 这个工具,模拟第三方服务的接口。比如测 “支付回调” 接口,我们需要模拟支付网关返回 “支付成功” 和 “支付失败” 两种响应。

先加依赖:

<dependency>

    <groupId>com.github.tomakehurst</groupId>

    <artifactId>wiremock-spring-boot-starter</artifactId>

    <version>2.35.0</version>

    <scope>test</scope>

</dependency>

再写测试:

// 启用WireMock,模拟第三方服务

@WireMockTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

public class PaymentCallbackControllerMockTest {

    @Autowired

    private TestRestTemplate restTemplate;

    // 场景1:模拟支付网关返回“支付成功”

    @Test

    void handlePaymentCallback_success() {

        // 1. 告诉WireMock:如果收到/post/gateway/callback的请求,就返回成功的JSON

        String mockSuccessResponse = "{\n" +

                "  \"orderId\": \"20240610001\",\n" +

                "  \"payStatus\": \"SUCCESS\",\n" +

                "  \"tradeNo\": \"PAY202406100001\"\n" +

                "}";

        WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/pay/gateway/callback"))

                .willReturn(WireMock.aResponse()

                        .withStatus(200) // 返回200状态码

                        .withHeader("Content-Type", "application/json")

                        .withBody(mockSuccessResponse)));

        // 2. 构造我们自己的回调请求(模拟支付网关调用我们)

        PaymentCallbackDTO callbackDTO = new PaymentCallbackDTO();

        callbackDTO.setOrderId("20240610001");

        callbackDTO.setPayStatus("SUCCESS");

        // 3. 调用我们的回调接口

        ResponseEntity<CallbackResultDTO> response = restTemplate.postForEntity(

                "/api/payment/callback",

                callbackDTO,

                CallbackResultDTO.class

        );

        // 4. 验证:我们的接口返回“处理成功”

        assertThat(response.getBody().getCode()).isEqualTo("SUCCESS");

    }

}

新手小贴士:

模拟异常场景:除了正常响应,还要模拟第三方服务超时(用withFixedDelay(3000)加 3 秒延迟)、返回 500 错误,看我们的接口能不能处理;

别在生产用 Mock:Mock 只在测试环境用,生产环境一定要连真实的第三方服务;

复用 Mock 配置:把常用的 Mock 规则(比如支付网关、短信服务)抽成公共类,不用每个测试都写一遍。

五、压测:提前暴露高并发下的 “隐形坑”

之前做秒杀活动,接口在测试环境好好的,一上线就崩了 —— 后来才知道,是没做压测,QPS 一上来,数据库连接池就满了。压测的作用就是模拟大流量,看接口在 “忙的时候” 能不能顶住。

怎么落地?

用 JMeter 做压测,Spring Boot 加 Actuator 暴露监控指标,方便看性能数据。

第一步:加监控依赖

<!-- 暴露性能指标 -->

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<!-- 支持Prometheus(可选,用来可视化监控) -->

<dependency>

    <groupId>io.micrometer</groupId>

    <artifactId>micrometer-registry-prometheus</artifactId>

</dependency>

配置 application.yml,开放监控端点:

management:

  endpoints:

    web:

      exposure:

        include: prometheus,health # 开放prometheus和健康检查端点

  metrics:

    tags:

      application: product-service # 标记服务名,方便区分

第二步:用 JMeter 压测

新建 “线程组”:比如设置 1000 个线程(模拟 1000 个并发用户),循环 10 次,10 秒内逐步启动(避免瞬间打垮服务);

加 “HTTP 请求”:填接口地址(比如/api/product/list?page=1&size=20),选 GET 方法;

加 “监听器”:比如 “聚合报告”,看平均响应时间、错误率这些关键指标。

关键指标怎么看?

指标合格标准出问题了怎么办?

平均响应时间小于 300 毫秒可能是 SQL 没加索引,或者没缓存

90% 响应时间小于 500 毫秒有长尾请求,比如个别用户查大列表

错误率小于 0.1%可能是参数错,或者服务限流了

QPS满足业务峰值不够就加机器,或者做服务拆分

新手小贴士:

压测环境要像生产:数据库数据量至少 10 万级,硬件配置(CPU、内存)跟生产差不多,不然结果不准;

逐步加压:别一上来就开 1 万线程,从 100、500、1000 逐步加,看什么时候性能开始下降;

压测时看监控:重点看 CPU、内存、数据库连接池、Redis 命中率,定位瓶颈在哪。

六、容错测试:依赖挂了,接口也不能崩

微服务里,一个服务依赖的 Redis、数据库或者其他服务挂了,很容易 “连锁反应” 导致整个接口崩掉。容错测试就是验证:当依赖出问题时,我们的接口能不能 “优雅降级”,而不是直接报错。

怎么落地?

用 Resilience4j 做熔断降级,Chaos Monkey 模拟依赖故障。以 “商品详情” 接口为例,假设它依赖 Redis,Redis 挂了就返回缓存的旧数据。

第一步:加依赖

<!-- 熔断降级框架 -->

<dependency>

    <groupId>io.github.resilience4j</groupId>

    <artifactId>resilience4j-spring-boot3</artifactId>

    <version>2.1.0</version>

</dependency>

<!-- 模拟故障的工具 -->

<dependency>

    <groupId>de.codecentric</groupId>

    <artifactId>chaos-monkey-spring-boot</artifactId>

    <version>2.6.2</version>

    <scope>test</scope>

</dependency>

第二步:接口加容错注解

@RestController

@RequestMapping("/api/product")

public class ProductController {

    @Autowired

    private ProductService productService;

    // 配置熔断:错误率超过50%就熔断,熔断后5秒再试

    @CircuitBreaker(name = "productDetail", fallbackMethod = "getProductDetailFallback")

    @GetMapping("/{id}")

    public ResponseEntity<ProductDTO> getProductDetail(@PathVariable Long id) {

        ProductDTO product = productService.getProductById(id); // 依赖Redis

        return ResponseEntity.ok(product);

    }

    // 降级方法:Redis挂了就走这里,返回旧数据

    public ResponseEntity<ProductDTO> getProductDetailFallback(Long id, Exception e) {

        log.error</doubaocanvas>

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容