CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
API 设计实战入门/课时/测试
RawBlame
课时目录12 节

API 设计实战入门

  1. 01API 基础
  2. 02HTTP 协议层
  3. 03REST 资源设计
  4. 04错误处理
  5. 05认证
  6. 06授权模型
  7. 07API 安全
  8. 08API 风格
  9. 09架构与集成
  10. 10测试
  11. 11可观测与合规
  12. 12文档与工具

测试

契约、Mock 与负载测试。

30 分钟

测试

这一课你将能做到

  1. 编写契约测试(Contract Testing),保证前后端接口约定不被破坏
  2. 使用 JMeter 或 Gatling 进行 API 负载测试,评估系统承载能力
  3. 用 Mock 和 WireMock 隔离外部依赖,编写可重复的集成测试

为什么要学

API 是多方协作的契约。前端按文档开发,后端改了字段没通知,上线就崩。契约测试自动检测这种破坏;负载测试告诉你系统能扛多少人;Mock 让测试不依赖外部服务,随时可跑。

先建立图像

测试金字塔(API 视角):

        /\
       /  \        E2E 测试(少量)
      /____\       模拟真实用户场景
     /      \
    /________\     契约测试(适量)
   /          \    前后端约定校验
  /____________\
 /              \  集成测试(多量)
/________________\ API + 数据库 + 外部服务

  单元测试(基石)
  Service/Repository 层逻辑

跟着做

步骤 1:单元测试——Service 层

@ExtendWith(MockitoExtension.class)
class LessonServiceTest {

    @Mock
    private LessonRepository lessonRepository;

    @InjectMocks
    private LessonService lessonService;

    @Test
    void shouldReturnLessonWhenIdExists() {
        // given
        Lesson lesson = Lesson.builder().id(1L).title("Spring Boot").build();
        when(lessonRepository.findById(1L)).thenReturn(Optional.of(lesson));

        // when
        Lesson result = lessonService.findById(1L);

        // then
        assertThat(result.getTitle()).isEqualTo("Spring Boot");
    }

    @Test
    void shouldThrowWhenLessonNotFound() {
        when(lessonRepository.findById(999L)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> lessonService.findById(999L))
                .isInstanceOf(LessonNotFoundException.class);
    }
}

步骤 2:集成测试——Controller + 数据库

@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class LessonControllerIntegrationTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
            .withDatabaseName("testdb");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);
    }

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private LessonRepository lessonRepository;

    @BeforeEach
    void setUp() {
        lessonRepository.deleteAll();
    }

    @Test
    void shouldCreateLesson() throws Exception {
        mockMvc.perform(post("/api/lessons")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"title": "Spring Boot 入门", "slug": "spring-boot-intro"}
                                """))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data.title").value("Spring Boot 入门"));

        assertThat(lessonRepository.findAll()).hasSize(1);
    }

    @Test
    void shouldReturn404WhenLessonNotFound() throws Exception {
        mockMvc.perform(get("/api/lessons/999"))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value(404))
                .andExpect(jsonPath("$.message").value("课程不存在"));
    }
}

步骤 3:契约测试——Spring Cloud Contract

定义契约(Groovy DSL):

// contracts/lesson/getLesson.groovy
Contract.make {
    description "获取课程详情"
    request {
        method GET()
        url "/api/lessons/1"
    }
    response {
        status OK()
        headers {
            contentType(applicationJson())
        }
        body([
            code: 200,
            message: "success",
            data: [
                id: 1,
                title: "Spring Boot 入门",
                slug: "spring-boot-intro"
            ]
        ])
    }
}
// contracts/lesson/createLesson.groovy
Contract.make {
    description "创建课程"
    request {
        method POST()
        url "/api/lessons"
        headers {
            contentType(applicationJson())
        }
        body([
            title: "Spring Security",
            slug: "spring-security"
        ])
    }
    response {
        status CREATED()
        headers {
            contentType(applicationJson())
        }
        body([
            code: 200,
            message: "success",
            data: [
                id: regex(number()),
                title: "Spring Security",
                slug: "spring-security"
            ]
        ])
    }
}

Maven 插件自动生成测试:

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>4.0.4</version>
    <extensions>true</extensions>
    <configuration>
        <baseClassForTests>com.codeforge.BaseContractTest</baseClassForTests>
    </configuration>
</plugin>
// 基类
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public abstract class BaseContractTest {

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void setup() {
        RestAssuredMockMvc.mockMvc(mockMvc);
    }
}

前端也可以用 Pact 做消费者驱动的契约测试(CDC)。

步骤 4:负载测试——JMeter

<!-- JMeter 测试计划关键配置 -->
<ThreadGroup>
    <stringProp name="ThreadGroup.num_threads">100</stringProp>  <!-- 100 并发用户 -->
    <stringProp name="ThreadGroup.ramp_time">10</stringProp>     <!-- 10 秒内启动 -->
    <stringProp name="ThreadGroup.duration">60</stringProp>      <!-- 持续 60 秒 -->
</ThreadGroup>

<HTTPSamplerProxy>
    <stringProp name="HTTPSampler.domain">localhost</stringProp>
    <stringProp name="HTTPSampler.port">8080</stringProp>
    <stringProp name="HTTPSampler.path">/api/lessons</stringProp>
    <stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>

命令行执行:

jmeter -n -t lesson-load-test.jmx -l result.jtl -e -o report/

关键指标:

  • 吞吐量(Throughput):每秒处理请求数
  • 响应时间(Response Time):平均、P90、P95、P99
  • 错误率(Error Rate):失败请求占比
  • 并发用户数(Concurrent Users):同时在线用户数

步骤 5:Mock 外部服务——WireMock

@SpringBootTest
@AutoConfigureWireMock(port = 0)
class ExternalServiceTest {

    @Autowired
    private WeatherService weatherService;

    @Test
    void shouldReturnWeather() {
        // 模拟外部天气 API
        stubFor(get(urlEqualTo("/api/weather?city=beijing"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody("""
                                {"city": "beijing", "temperature": 25, "condition": "sunny"}
                                """)));

        Weather weather = weatherService.getWeather("beijing");

        assertThat(weather.getTemperature()).isEqualTo(25);
    }
}

讲清楚要点

  1. 契约测试是前后端的「合同」:前端按契约开发,后端按契约实现,任何一方破坏契约,CI 立即失败。Spring Cloud Contract 适合 JVM 栈,Pact 适合多语言。
  2. 集成测试要用 Testcontainers:真实数据库(Docker 启动 MySQL),而不是 H2 内存库。H2 和 MySQL 行为有差异,测试通过不等于生产安全。
  3. 负载测试关注 P99,不是平均值:平均响应 100ms 看起来很好,但如果 P99 是 5s,说明有 1% 的用户体验极差。优化长尾比优化平均更重要。
  4. Mock 外部依赖,不是 Mock 自己的代码:测试 LessonService 时,Mock LessonRepository;测试 LessonController 时,Mock LessonService 或真实调用。不要 Mock 被测对象本身。
  5. 测试数据要隔离:每个测试方法独立数据,用 @Transactional 回滚或 @Sql 脚本清理。共享数据导致测试顺序依赖,极难排查。

你可能会懵的地方

  • 单元测试和集成测试边界在哪? 单元测试:单类、单方法、全 Mock、毫秒级;集成测试:多类协作、真实数据库/HTTP、秒级。Controller 测试属于集成测试。

  • 契约测试能替代集成测试吗? 不能。契约测试只验证「接口格式」,不验证「业务逻辑」。比如契约规定返回 {id, title},但 title 内容对不对,需要集成测试或单元测试。

  • 负载测试和性能测试有什么区别? 负载测试:正常压力下系统的表现(如 100 并发);压力测试:极限压力(如 10000 并发,直到系统崩溃);稳定性测试:长时间运行(如 24 小时,看内存泄漏)。

  • WireMock 和 Mockito 有什么区别? Mockito Mock 的是「Java 对象」(方法调用);WireMock Mock 的是「HTTP 服务」(网络请求)。测试外部 API 用 WireMock,测试内部依赖用 Mockito。

  • 测试覆盖率多少合适? 核心业务(如订单、支付)80%+;工具类、Controller 60%+;Getter/Setter、配置类不强制。不要盲目追求 100%,测试也有维护成本。

错法对照

错法问题正解
所有测试都走 Controller + 真实数据库测试慢,几十分钟跑不完金字塔:大量单元测试 + 适量集成测试 + 少量 E2E
测试共享数据,一个失败全挂顺序依赖,排查困难每个测试独立数据,@Transactional 回滚
Mock 所有外部依赖,不测真实调用外部接口变了测试发现不了契约测试 + 少量真实调用(如每天跑一次的冒烟)
负载测试只看平均值长尾问题被掩盖关注 P90/P95/P99,优化慢查询
测试代码和生产代码一样复杂维护困难,没人愿意改测试代码要简单直接,允许一定的重复

卡点急救

  1. Testcontainers 启动慢:复用容器(@Container 用 static),或本地安装数据库(CI 用容器,本地用已安装的)。
  2. 契约测试和实际实现不一致:契约是「理想状态」,实现是「实际状态」。以契约为准,实现向契约对齐,或更新契约(需前后端协商)。
  3. JMeter 压测时本机成瓶颈:JMeter 本身消耗资源。用分布式压测(多台 JMeter 机器),或换 Gatling(基于 Netty,性能更好)。

面试常问

  • 怎么保证前后端接口不脱节? 契约测试(Spring Cloud Contract / Pact)+ OpenAPI 文档生成。CI 中自动校验,破坏契约则构建失败。

  • 负载测试怎么做?关注哪些指标? 工具:JMeter、Gatling、k6。指标:吞吐量、响应时间(P90/P95/P99)、错误率、资源利用率(CPU/内存/磁盘/网络)。能讲出「先单接口压测,再全链路压测」的流程。

  • 测试金字塔是什么? 底部大量单元测试(快、便宜),中部适量集成测试(中速),顶部少量 E2E 测试(慢、贵)。反例是「冰淇淋蛋筒」:大量 E2E,少量单元,维护噩梦。

自测三问

  1. 设计一个「课程搜索」接口的测试策略,需要哪些类型的测试?
  2. 如果负载测试发现 P99 响应时间从 100ms 涨到 2s,怎么排查?
  3. 为什么契约测试不能替代单元测试?

小结一句话

测试是 API 质量的保险:单元测试保逻辑,契约测试保约定,负载测试保容量,Mock 保隔离;金字塔结构让测试又快又稳。

延伸

  • Spring Cloud Contract 文档
  • Testcontainers 官方文档
  • JMeter 用户手册
  • 下一课:可观测与合规——限流、监控、日志与 GDPR。
加载本节练习…
← 上一课架构与集成下一课 →可观测与合规