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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
软件测试入门/课时/综合练习
RawBlame
课时目录12 节

软件测试入门

  1. 01测试金字塔
  2. 02JUnit 5 基础
  3. 03Mockito 与 Mock
  4. 04Spring Boot 测试
  5. 05MockMvc 接口测试
  6. 06集成测试
  7. 07TDD 与可维护性
  8. 08前端单元测试
  9. 09E2E 与 Playwright
  10. 10覆盖率与质量门禁
  11. 11CI 中的测试
  12. 12综合练习

综合练习

为 tracks API 写 MockMvc + 集成测试。

40 分钟

综合练习

这一课你将能做到

  1. 为判题接口交付 ≥4 个分层测试:SubmissionService Mockito 单测 + SubmissionController MockMvc 契约测
  2. 用定向 mvn test -Dtest=... 与提交前 mvn verify 证明本地绿,并解释每一层的取舍理由
  3. 推送后确认 ci.yml 相关 job 全绿,并写出「新人如何只跑本模块测试」的三行说明

为什么要学

前面各课是零件;本课要求你串成可交付物。人话:会背金字塔不够,要能在真实模块上留下绿测,并让 CI 替你守门。

目标对象是 CodeForge 的判题接口(SubmissionController + SubmissionService + SubmissionMapper 边界)——它是「校验 + 状态机 + 落库」的浓缩样本,四层测试各有一席之地。注意现有 SubmissionServiceTest 已覆盖「构造记录并置 pending」和「按用户课时过滤」,你的任务是补缺口而不是复制粘贴:非法输入分支、静态登录态、HTTP 契约、错误体形状。

完成后,你应能向同伴演示「测在哪、怎么跑、CI 看哪」——这正是面试里「聊聊你们项目的测试体系」的现场答案。

先建立图像

交付清单:

[1] SubmissionServiceExtraTest   Mockito:非法语言 / 非法课时
[2] SubmissionControllerSliceTest MockMvc:200 JSON / 400 错误体 / 401
[3](选做)UserMapperIT           Testcontainers:唯一索引
[4](选做)VerdictBadge.test.tsx  Vitest:徽章渲染

验收:本地定向绿 → mvn verify 绿 → push → CI 绿 → 三行说明
层工具最低条数
Service@Mock + @InjectMocks2
HTTPMockMvc + @MockitoBean + mockStatic2
集成 / 前端Testcontainers IT / Vitest选做各 1

跟着做

步骤 1:盘点现有测试,选定缺口

打开 backend/src/test/java/com/codeforge/service/SubmissionServiceTest.java,笔记两列:

  • 已有:submit_构造记录并置pending、listByUserAndLesson_按用户和课时过滤
  • 缺口:非法语言、空代码、非法课时、HTTP 400 错误体、未登录 401

选定两个 Service 场景 + 两个 HTTP 场景为必做。可以加强既有断言,但不要重复完全相同的用例。

步骤 2:Service 层两个缺口场景(Mockito)

@ExtendWith(MockitoExtension.class)
class SubmissionServiceExtraTest {

    @Mock SubmissionMapper submissionMapper;
    @InjectMocks SubmissionService service;

    @Test
    void submit_不支持的动画语言被拒绝且不落库() {
        // 语言白名单外的输入:校验前置,无需 stub
        assertThatThrownBy(() -> service.submit(3L, 5L, "brainfuck", "code"))
            .isInstanceOf(BusinessException.class)
            .hasMessage("不支持的语言类型");

        verify(submissionMapper, never()).insert(any(Submission.class));
    }

    @Test
    void submit_课时非法时拒绝并给出提示() {
        assertThatThrownBy(() -> service.submit(3L, 0L, "java", "code"))
            .isInstanceOf(BusinessException.class)
            .hasMessageContaining("课时参数");

        verify(submissionMapper, never()).insert(any(Submission.class));
    }
}
mvn test -Dtest=SubmissionServiceExtraTest

应看到:两例秒级绿。方法名、异常类型按你仓库真实代码对齐,意图(非法输入 → 拒绝 + 不落库)不变。

步骤 3:HTTP 契约两个场景(MockMvc 切片)

@WebMvcTest(SubmissionController.class)
class SubmissionControllerContractTest {

    @Autowired MockMvc mockMvc;
    @MockitoBean SubmissionService submissionService;

    @Test
    void 提交成功返回200与pending记录() throws Exception {
        try (MockedStatic<StpUtil> stp = mockStatic(StpUtil.class)) {
            stp.when(StpUtil::getLoginIdAsLong).thenReturn(3L);

            Submission s = new Submission();
            s.setId(10L);
            s.setStatus("pending");
            when(submissionService.submit(3L, 5L, "java", "class A {}"))
                .thenReturn(s);

            mockMvc.perform(post("/submissions")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("""
                        {"lessonId":5,"language":"java","code":"class A {}"}
                        """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(10))
                .andExpect(jsonPath("$.status").value("pending"));
        }
    }

    @Test
    void 业务异常返回400与统一错误体() throws Exception {
        try (MockedStatic<StpUtil> stp = mockStatic(StpUtil.class)) {
            stp.when(StpUtil::getLoginIdAsLong).thenReturn(3L);
            when(submissionService.submit(any(), any(), any(), any()))
                .thenThrow(new BusinessException("不支持的语言类型"));

            mockMvc.perform(post("/submissions")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("""
                        {"lessonId":5,"language":"brainfuck","code":"x"}
                        """))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.message").value("不支持的语言类型"));
        }
    }
}

这一组测试证明的是 GlobalExceptionHandler 的翻译契约——前端解析错误提示全靠它。

步骤 4:提交前全量对齐 + 选做加分项

cd backend
mvn verify        # 快测 + IT + JaCoCo 报告,与 CI 同命令

# 选做 A:Testcontainers 唯一索引(见第 6 课 UserMapperIT)
# 选做 B:platform 组件测(见第 8 课 VerdictBadge)
cd ../platform && npm test

选做项做之前先想清楚它守的是哪层缺口,说不清理由的选做项宁可不加——这句话本身就是金字塔训练。

步骤 5:推送并做 CI 自检 + 写三行说明

推送功能分支,打开 GitHub Actions:

  • backend job:mvn -B verify 绿
  • platform job:npm test 绿(若你动了前端)

在 PR 描述或模块 README 写下:

只跑判题相关:mvn test -Dtest=Submission*
提交前全量:mvn verify
看 CI:.github/workflows/ci.yml 的 backend / platform job

这三行是给下一个接手的人的第一份礼物——它让「我该怎么验证我的改动」不再口口相传。

步骤 6:对照评分表自检(自学用)

项标准
命名@Test 名描述行为,失败报告可读
独立无顺序依赖、无共享脏状态
速度Service 层不起全上下文
断言具体字段 / 异常消息,不只 verify
分层每条测试能说清「为什么放这层」
CI相关 job 绿

任何一条不过关,回到对应课重练——综合练习的意义是把薄弱环节暴露出来。

讲清楚要点

  1. 分层职责不同,所以 Service 测与 MockMvc 测都要交:只交一层刷数量,验收看的是结构完整性。
  2. 命名是给失败报告读的:业务异常返回400与统一错误体 让 CI 红灯自解释——综合交付按这个标准执行。
  3. 测试必须独立:@BeforeEach 或纯 stub,禁止用例间顺序依赖——顺序在本地和 CI 上都可能不同。
  4. 速度决定愿不愿意跑:Service 测不要随手 @SpringBootTest,容器留给 HTTP/集成层。
  5. 断言质量重于数量:只 verify 无业务 assert 的空壳测是负资产——它让覆盖率虚高、让审查者误信。
  6. CI 是最终裁判:本地 verify 与 workflow 命令对齐,「推上去才红」是最贵的反馈路径。

你可能会懵的地方

  • 现有测试已经覆盖了我写的场景,还要交吗? 换缺口:边界、错误码、缺字段。练习的是完整闭环,不是重复造轮子;加强既有断言也是合格交付。

  • 路径 / 字段与示例不一致? 以仓库真实 API 为准调整路径与 JSON 字段,四条用例的意图不变:非法拒绝、契约锁定。

  • platform 没动,CI platform 却红了? 先确认是否与你改动相关(rebase 最新 main 再看);主线既有问题在 PR 里说明归属,别默默修不属于你的锅。

  • 怎样算完成? 必做 4 条 @Test 全绿 + 能讲解每条分层理由 + CI 相关 job 绿。选做 IT 与组件测是加分项,不是门槛。

错法对照

错法问题正解
四条测全是 @SpringBootTest 打真库慢、难维护Service 纯 Mockito
只 verify 无 assert假绿断言返回值 / 状态码 / 字段
方法名 test1、t1失败不可读行为命名
本地定向绿就直接推CI 惊魂提交前 mvn verify
红灯删测过关失去练习意义修实现或修断言

卡点急救

  1. 切片测报缺 Bean:对照第 4 课,把 Controller 构造器依赖逐个 @MockitoBean;StpUtil 静态调用配 mockStatic。
  2. 定向命令报 No tests found:-Dtest= 的模式与实际类名不匹配(Submission* 覆盖不到 SubmissionControllerContractTest 时改用 Submission*Test 或逗号列出)。
  3. 本地绿 CI 红:读第一个失败堆栈(第 11 课五步法),90% 是本地没跑全量 verify 或依赖版本漂移。

面试常问

  • 用一段话介绍你们项目的测试体系(现场模板)。 「CodeForge 分四层:Service 层用 JUnit 5 + Mockito 做毫秒级单测,覆盖校验与状态分支;HTTP 层用 MockMvc + @MockitoBean 锁定状态码与统一错误体契约;数据层用 Testcontainers 起真实 MySQL 验证唯一索引与往返;前端用 Vitest + Testing Library 测组件行为,Playwright 只守注册、判题等三到五条命门。CI 里 PR 跑 mvn verify 与 npm test 阻断合并,慢测分轨夜间。」——按自己项目改细节,骨架可直接背。

  • 给判题接口写测试,你会先写哪几条?为什么? 顺序:非法输入拒绝(最便宜、最高频回归)→ 成功路径落库状态 → HTTP 契约(状态码 + 错误体)→ 唯一性 / 数据往返(集成)。排序逻辑:成本从低到高,回归风险从高频到低频。

  • 怎么向新人解释「该补哪层测试」? 给决策规则而不是给答案:改校验分支 → Mockito 单测;改 SQL/迁移 → Testcontainers IT;改 HTTP 错误翻译 → MockMvc;改整页交互 → 组件测或 E2E。新人拿着规则自己能走,比每问一次答一次可持续。

自测三问

  1. 你交付的 4 条测试分别守金字塔的哪一层?各证明了什么单层证不了的事?
  2. 为什么「存在 + 不存在」比两条 happy path 更有回归价值?
  3. 新人只想跑判题相关测试,你给的 Maven 命令是什么?

小结一句话

Mockito 守业务、MockMvc 守契约、Testcontainers 守数据、Vitest/Playwright 守界面、CI 守合并——testing-essentials 以一次完整交付闭环收束。

延伸

  • Spring Boot Testing
  • JUnit 5 User Guide
  • 实际下一步:把这套节奏带回日常——给下一个真实 feature 走「红 → 绿 → 重构」,让每次 PR 都带着分层测试。想继续深入,可沿三个方向:契约测试(Pact,锁定前后端契约)、性能测试(k6 / JMeter,压判题接口吞吐)、变异测试(PIT,检验测试自身的断言强度)。姊妹课:spring-boot-essentials 深入框架,react-essentials 与 nodejs-essentials 补前端与 Node 运行时。
加载本节练习…
← 上一课CI 中的测试