老陈是村里最出名的木匠,盖了三十年房子从没塌过。秘密不在手艺多巧,而在他有个“笨”习惯:每一块木板进屋前,都要拿小锤敲一敲、眯着眼看两端、在木料库里对着图纸清点一遍。邻居笑他多此一举,可有一次一块看似完好的房梁,一敲发出“闷闷”的声音,切开里面早就朽了。若等它上了屋顶,整间房都可能倒塌。写代码也是这样——好代码不是“写”出来的,而是“测”出来的。单元测试,就是老陈手里那把敲木头的小锤。 这篇文章会带你走进“代码质检”的世界:什么是单元测试、怎样设计测试用例、如何用 Mock 隔离依赖、边界条件为什么最容易藏 bug、覆盖率该怎么看,以及如何把测试变成团队的工程习惯。全程用老陈盖房的故事来打比方,保证零基础也能看懂。

岗位关联

学会单元测试,等于给代码装上一台“体检仪”。改代码不再提心吊胆,交付质量有底气,这是 AI 工程师从“能跑”走向“靠谱”的必修课。

目录
  1. 可视化演示
  2. 测试基本概念
  3. 测试用例设计
  4. Mock技巧
  5. 边界条件覆盖
  6. 覆盖率度量
  7. 测试工程化
  8. 测试可维护性
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示单元测试的AAA用例设计模式、Mock依赖隔离技巧和代码覆盖率分析,通过模式切换理解测试工程的核心实践。

本节配有交互动效(进入视口后自动加载,约 3KB)。

AAA用例设计模式 Arrange准备测试数据和依赖 Act执行被测方法 Assert验证结果符合预期

测试基本概念

老陈有个雷打不动的规矩:每根木头进屋前先敲一敲。这不是仪式感,是三十年的经验——木头看着光滑,敲着发“空”就得警惕,内核裂了的地方,手摸不出来,但声音骗不了人。软件世界也一样:函数看起来没问题,一传入奇怪的参数就悄悄返回了错误结果。测试,就是那把能“敲出内伤”的小锤。

软件里的“敲木头”叫做单元测试。它把程序里一个最小的代码单元(通常是一个函数或一个类的方法)单独拎出来,喂给它固定的输入,再检查它吐出的输出是否符合预期。它不弹界面、不连数据库、不请求网络,只关心这一段代码自己站不站得住。在 AI 工程里,数据清洗、特征计算、模型推理封装这些函数,每个都值得先“敲一敲”再上生产。

测试界的经典坐标系叫“测试金字塔”,它把测试分成三层:最底层是海量单元测试,中间是少量集成测试,顶层是极少数端到端测试。单元测试跑得最快、定位最准、成本最低,所以撑起金字塔的地基。同时要分清“测试”与“调试”:测试负责发现问题,调试负责定位并修复问题——一个是体检,一个是手术。

老陈不敲每根木头的每一个毛孔,但他保证每根木头都敲过。单元测试就是那把不厌其烦的小锤——用最便宜的动作,挡住最贵的灾难。

Nunit:Nintegration:Ne2e=7:2:1N_{unit} : N_{integration} : N_{e2e} = 7 : 2 : 1
这个比例公式是行业经验值:单元测试 7 份、集成测试 2 份、端到端测试 1 份。金字塔越往下越便宜越快速,越往上越昂贵越脆弱,比例失衡通常意味着测试策略出了问题。
  1. N_unit(单元测试数量)金字塔最底层,数量最多,毫秒级跑完,失败时能精确指出哪个函数坏了。
    Nunit=7N_{unit} = 7
  2. N_integration(集成测试数量)验证模块之间能不能配合,数量明显少于单元测试。
    Nintegration=2N_{integration} = 2
  3. N_e2e(端到端测试数量)模拟完整用户流程,成本最高、最脆弱,数量最少。
    Ne2e=1N_{e2e} = 1
  4. 比例关系7:2:1 提醒你,如果端到端测试堆得比单元测试还多,就该反思测试策略了。
    Nunit:Nintegration:Ne2e=7:2:1N_{unit} : N_{integration} : N_{e2e} = 7 : 2 : 1

测试用例设计

老陈检查木材要分种类:松木看虫眼,橡木看裂纹,湿料看变形。他不可能把每块木头翻来覆去查一百遍,但分好类、每类抽一块代表性的检查,就能覆盖绝大多数问题。这种“分类抽查”的智慧,正是测试用例设计的精髓。

所谓测试用例设计,就是把无穷无尽的输入空间压缩成有限几个必测点。你不可能把全世界所有数字都喂给一个函数,但你可以把输入“分门别类”,每一类挑一个代表来测。经典的三个工具箱是:等价类划分、边界值分析、因果图方法。等价类划分把输入按行为分成“有效等价类”(程序该受理的)和“无效等价类”(程序该拒绝的);边界值分析专门盯着取值范围的两端;因果图则处理多个条件组合出不同结果的复杂场景,避免重复冗余的用例。

设计用例时新手最容易犯的错是只测“正常情况”:传个 100 进去,返回 90,绿灯一亮就收工。可程序真正翻车的地方,全在那些“合法之外”的输入上。无效等价类测的是程序的“脾气”——收到不合理的输入时,是优雅地抛出异常,还是直接崩给你看?

老陈分木料、分情况、分类抽查,你的测试也照这个思路来:先分组,再抽代表,最后把合法与非法两条路都走一遍。分类清晰了,一敲一个准。

Nclass=Nvalid+NinvalidN_{class} = N_{valid} + N_{invalid}
测试用例的总数,等于有效等价类数量加上无效等价类数量。两个阵营缺一不可:只测合法输入,等于只考学生“会做的题”,永远发现不了短板。
  1. N_class(用例总数)设计出的测试用例数量,等于两类等价类数量之和。
    NclassN_{class}
  2. N_valid(有效等价类)合法输入按行为分成几组,每组取一个代表值来测。
    NvalidN_{valid}
  3. N_invalid(无效等价类)非法输入也要分组覆盖,比如负数、空值、超长文本。
    NinvalidN_{invalid}
  4. 相加关系两类都要测,缺了无效等价类,异常分支就永远裸奔。
    Nclass=Nvalid+NinvalidN_{class} = N_{valid} + N_{invalid}

Mock技巧

老陈要单独验收一根梁的卯榫是否合格,不会干等隔壁的柱子装好了再来试——他手里有一套标准卯口样板,随时拿起来一卡,合不合格当场见分晓。这套样板,就是测试世界里的 Mock。

被测函数往往有一堆“邻居”:要连数据库、要调第三方支付接口、要读取系统当前时间。这些邻居要么太慢、要么太贵、要么行为不可控。Mock 就是一个伪造的邻居,它的接口长得和真邻居一模一样,但行为由你说了算——你想让它返回 500 就返回 500,想让它超时就超时。这样一来,被测函数被装进“真空实验室”,跑得快、结果稳。

Mock 的三大用途:第一,隔离外部依赖,测试不连数据库、不发网络请求,秒级跑完;第二,模拟稀缺场景,比如让支付接口故意报错,验证你的降级逻辑;第三,行为验证,除了看返回值,还可以断言“某个方法被调用了几次、传了什么参数”。但要记住,Mock 验证的是“形似”而非“神似”——它必须和真实依赖的接口契约保持一致,否则就是自欺欺人。

标准卯口样板让老陈在工位上就能验梁,Mock 让开发者不启动整个系统就能验函数。先把零件验明白,再谈整栋楼的装配。

R=f(a,D~),D~≈DR = f(a, \tilde{D}), \quad \tilde{D} \approx D
被验证的结果 R,等于函数 f 在替身依赖 \tilde{D} 下的返回值。\tilde{D} 只需“长得像”真实依赖 D,行为完全由测试控制,这正是隔离测试的前提。
  1. f(a, D)被测函数 f 拿着入参 a 去调用真实依赖 D,D 可能是数据库或外部服务。
    f(a,D)f(a, D)
  2. \tilde{D}(替身依赖)Mock 对象,接口与 D 一致,行为由测试代码编程指定。
    D~≈D\tilde{D} \approx D
  3. R(验证结果)在替身依赖下运行函数,得到的返回值必须符合测试预期。
    R=f(a,D~)R = f(a, \tilde{D})
  4. ≈ 的含义Mock 只求“长相一致”不求“功能一致”,这正是可以把真实依赖安全换掉的底气。
    D~≈D\tilde{D} \approx D

边界条件覆盖

房子最容易出问题的地方永远是边角:墙角的雨水渗漏、窗框的接缝开裂、梁头的榫口松动。老陈验收的标准动作是“挑两头”——每根梁两端的榫头各敲三下,中间反而不那么费心。代码里的 bug 也偏爱边界:循环恰好取到最后一个元素、金额刚好等于阈值、字符串长度刚好是最大值。

边界条件覆盖,就是专门在取值范围的两端做文章。经典陷阱包括:空值处理,用户没传参数怎么办;极值边界,金额取到最大值、数组取到超大容量;异常输入,负数、溢出、格式错误。这类 bug 的共同特点是“差一点”——判断条件少写了等号,循环多走了一轮,也就是传说中的 off-by-one 错误。

对付边界问题有一把标准尺子:对任何合法取值区间,至少要测六个点。比如打折接口规定价格区间是 0 到 10000,那 -1、0、1、9999、10000、10001 这六个值一个都不能少,每一个都有它必须被“正确对待”的理由。

老陈敲梁头,你看数组的两端和阈值的两侧。边界守住了,中间大概率塌不了——这是代码世界里最划算的投资。

B={Vmin−1,  Vmin,  Vmin+1,  Vmax−1,  Vmax,  Vmax+1}B = \{V_{min}-1,\; V_{min},\; V_{min}+1,\; V_{max}-1,\; V_{max},\; V_{max}+1\}
边界值用例集合:最小值减一、最小值、最小值加一、最大值减一、最大值、最大值加一。边界外侧的两个点用来验证“优雅拒绝”,边界内侧的两个点用来验证“正常受理”。
  1. V_min 与 V_max合法取值的最小值和最大值,比如数组下标 0 与 length-1。
    Vmin,  VmaxV_{min},\; V_{max}
  2. V_min-1 与 V_max+1边界外第一格,程序应当优雅地把它们挡在门外。
    Vmin−1,  Vmax+1V_{min} - 1,\; V_{max} + 1
  3. V_min+1 与 V_max-1边界内侧第一格,确保合法的“擦边球”不被误伤。
    Vmin+1,  Vmax−1V_{min} + 1,\; V_{max} - 1
  4. B 集合把六个特殊值一起纳入用例,边界问题基本就全覆盖了。
    B={Vmin−1,Vmin,Vmin+1,Vmax−1,Vmax,Vmax+1}B = \{V_{min}-1, V_{min}, V_{min}+1, V_{max}-1, V_{max}, V_{max}+1\}

覆盖率度量

老陈有一本验收账本,每验完一块木头就画一个勾。月底一翻:100 根木头只验了 60 根,剩 40 根是“没验过”的,他心里立刻有数——还有四成风险没排查。账本上的百分比,对应到代码世界就是覆盖率。

覆盖率,通俗讲就是“测试运行时,到底有多少代码被执行过”。它是一份体检报告,告诉你身体哪些部位检查过了、哪些部位还是盲区。最常见的两个指标:行覆盖率看“每行代码有没有被执行”,分支覆盖率看“if 和 else 这两条岔路是不是都走过”。工具方面,Python 用 pytest-cov,Java 用 JaCoCo,前端用 Istanbul。

但覆盖率有个大坑:它只统计“执行过”,不统计“验证对”。一行代码被跑了一百遍,如果断言永远只检查返回值大于零,那它依然是裸奔。行业里常设 80% 的门槛,但这个数字的意义是“划定盲区警戒线”,而不是“通关免检章”。看到 100% 覆盖率,正确的反应不是欢呼,而是追问:每条分支都真正验证了正确的行为吗?

账本上的勾是老陈信心的来源,覆盖率是你的“测试账本”。但记住,账本只记录“验过没有”,不记录“验得多认真”。

Cline=LtestedLtotal×100%C_{line} = \frac{L_{tested}}{L_{total}} \times 100\%
行覆盖率 = 被测试执行到的代码行数 ÷ 总代码行数 × 100%。它衡量测试的“触及面”,却不衡量断言的“穿透力”。
  1. L_tested(已执行行数)测试运行时实际经过的代码行,由覆盖率工具自动统计。
    LtestedL_{tested}
  2. L_total(总行数)项目可执行代码的总行数,注释和空行不参与计算。
    LtotalL_{total}
  3. 相除已测行数占总行数的比例,反映测试触及面的宽度。
    LtestedLtotal\frac{L_{tested}}{L_{total}}
  4. 乘 100%换算成百分比便于设定门槛。建议结合分支覆盖率一起看。
    Cline=LtestedLtotal×100%C_{line} = \frac{L_{tested}}{L_{total}} \times 100\%

测试工程化

一个人盖房怎么干都行,但老陈的工程队有二十号人,各行其事必然出乱子。于是工地立下规矩:每天早会分工、材料入库登记、每周联合检查、谁负责的部件出问题追责到人。流程,让手艺变成了工程。测试也一样,从“我随手写了个脚本”到“团队规范地测”,中间隔着一整套工程化体系。

测试工程化的核心三件套:测试框架、持续集成、测试规范。框架是统一的“语言”,Python 用 pytest,Java 用 JUnit,前端用 Jest,大家写出来的测试风格一致、能自动发现、能汇总报告。持续集成(CI)是门禁——每次 git push 都自动拉取代码、跑全部测试,红灯亮了就阻断合并,把问题挡在合入主分支之前。规范则是约定俗成的章法:测试文件放哪、怎么命名、测试数据怎么管理。

工程化还有一个经典节奏叫 Red-Green-Refactor:先写一个必然失败的测试(红灯),再写最简代码让它通过(绿灯),然后放心重构(保持全绿)。这个循环把“写代码”和“写测试”焊在了一起,让测试不再是事后补作业,而是前进的导航仪。

老陈的工地规矩,对应到你仓库里的 CI 流水线、测试目录和红绿循环。把个人手艺沉淀成团队流程,质量才不会随人员流动而流失。

Pescape=UuntestedUtotalP_{escape} = \frac{U_{untested}}{U_{total}}
缺陷逃逸率 = 没有测试保护的功能单元数 ÷ 系统总功能单元数。想降低线上事故率,就持续压缩分子——让每一个功能单元都有测试兜底。
  1. U_untested(未测单元数)没有测试保护的功能模块,是 bug 逃逸到生产环境的窗口。
    UuntestedU_{untested}
  2. U_total(总单元数)系统中全部功能单元的数量。
    UtotalU_{total}
  3. 比值未测单元占比越高,回归风险越大,上线越要谨慎。
    Pescape=UuntestedUtotalP_{escape} = \frac{U_{untested}}{U_{total}}
  4. 工程意义CI 门禁每天做的事,就是让这个比例无限趋近于零。
    Uuntested→0⇒Pescape→0U_{untested} \to 0 \Rightarrow P_{escape} \to 0

测试可维护性

老陈的账本越记越厚,如果只有他自己看得懂,他一休假,新来的工匠准抓瞎。所以他从第一页就定下统一格式:日期、木材编号、问题描述、处理结果。每个字都是写给未来的同行看的。测试代码是同样的道理——它是写给明天看的,而“明天”可能是你自己,也可能是接手你代码的陌生人。

测试代码也是代码,它会被阅读、被修改、被维护。写得烂的测试比不写更痛苦:天天随机报错,你天天“修测试”,耗尽耐心后干脆删掉,防线就此崩塌。三条可维护性准则能救你于水火:命名即文档,test_订单金额超过100_自动减20 失败时,一眼就知道业务逻辑哪一环坏了;断言要精准,一个测试只验证一个行为,用 assertEqual 而不是含糊的 assertTrue;避免脆弱断言,不依赖实现细节、不依赖执行顺序、不用 sleep 等时间,把“时好时坏”的脆弱测试消灭在萌芽里。

测试重构也遵循 Red-Green-Refactor:先有绿灯保护,再大胆调整测试代码的结构。删除重复的夹具代码、提炼公共的构造方法、把过长的测试拆成多个意图单一的小测试——每一步重构都以“仍然全绿”作为安全网的反馈。

老陈的账本是写给后来人的,你的测试是写给明天自己的。一份清晰、稳定、易读的测试,才是能陪你走完整个项目周期的老伙计。

Dtrust=Cbranch×(1−Rflaky)D_{trust} = C_{branch} \times (1 - R_{flaky})
团队对测试的信任度 = 分支覆盖率 ×(1 - 脆弱测试比例)。覆盖率再高,如果测试动不动“抽风”闪红闪绿,团队的信任也会被归零。
  1. C_branch(分支覆盖率)测试走过了多少条 if/else 岔路,衡量测得多全。
    CbranchC_{branch}
  2. R_flaky(脆弱测试比例)时绿时红的“抽风”测试占比,是信任的头号杀手。
    RflakyR_{flaky}
  3. 1 - R_flaky(可信比例)测试结论中可靠的部分,脆弱测试越多可信度越低。
    1−Rflaky1 - R_{flaky}
  4. 相乘覆盖率和稳定性缺一不可,任何一项趋近于零,信任度都归零。
    Dtrust=Cbranch×(1−Rflaky)D_{trust} = C_{branch} \times (1 - R_{flaky})

AI 应用场景

特征工程函数的回归保护

阶段:数据预处理与特征工程

解决的问题:特征清洗、归一化、离散化这些函数是模型的地基,传统方式靠“肉眼 review + 上线后观察”发现错误,往往要等到模型效果波动才后知后觉。单元测试在每次改动特征逻辑后自动跑一遍,让“改 A 伤 B”的连锁 bug 在合并前就被拦截。

具体案例:某推荐系统团队对“用户行为序列截断函数”写了一组边界测试——序列为空、长度为 1、长度正好等于截断阈值、长度超出一万条。某次优化代码时修改了下标逻辑,测试立刻红灯:空序列场景返回值变成了 None。修复后再提交,全程不到五分钟,而以往这种 bug 要等线上指标下跌才开始排查。

模型策略切换前的回归测试

阶段:模型迭代与策略上线

解决的问题:每次调整模型参数或重排逻辑都有“把好的改坏”的风险。传统方式靠离线指标对比,但指标对极小概率的异常输入不敏感。用单元测试把关键规则固化成断言,毫秒级验证策略是否仍然守住了底线。

具体案例:某风控团队维护一个“授信额度计算函数”,里面有价格上下限、黑名单校验等 20 条规则。每次调整权重,都要重新验证一条 200 行的测试用例集:额度不为负、黑名单用户返回拒绝、非整数输入抛异常等。规则变更后一键全绿,团队才敢安心发布新策略。

AI 推理服务中的 Mock 依赖验证

阶段:模型服务化部署与联调

解决的问题:推理服务常依赖外部模型鉴权、特征存储、日志上报等服务,联调阶段这些依赖不稳定或未就绪。传统方式让开发干等,或搭建额外测试环境。用 Mock 模拟这些外部服务,服务端逻辑可以提前开发、提前验证。

具体案例:某 NLP 服务上线前,特征存储接口还未就绪。后端开发用 Mock 返回固定特征向量,先把推理链路的超时重试、降级策略全部测通,同时用 Mock 断言“每次推理恰好调用一次特征服务”。真实接口上线后,替换 Mock 跑一遍同样的用例,一次通过。

Python 演示

python49 行
# -*- coding: utf-8 -*-"""单元测试入门演示: 会员折扣计算器的测试设计"""import unittestfrom unittest.mock import Mock  def calc_discount(price, level):    """根据会员等级计算折后价, 展示等价类与边界值思想"""    if price is None or price < 0:        raise ValueError("price 不能为空或负数")    if level not in ("normal", "vip", "svip"):        raise ValueError("未知会员等级")    rate = {"normal": 1.0, "vip": 0.9, "svip": 0.8}[level]    return round(price * rate, 2)  class TestCalcDiscount(unittest.TestCase):    """等价类: normal/vip/svip 三类各取一例"""     def test_normal_keeps_price(self):        self.assertEqual(calc_discount(100, "normal"), 100.0)     def test_vip_gives_ten_percent_off(self):        self.assertEqual(calc_discount(100, "vip"), 90.0)     def test_svip_gives_twenty_percent_off(self):        self.assertEqual(calc_discount(100, "svip"), 80.0)     def test_boundary_zero_price(self):        self.assertEqual(calc_discount(0, "vip"), 0.0)     def test_boundary_negative_price_raises(self):        with self.assertRaises(ValueError):            calc_discount(-0.01, "vip")     def test_invalid_level_raises(self):        with self.assertRaises(ValueError):            calc_discount(100, "boss")     def test_mock_notifier_verifies_call(self):        notifier = Mock()        notifier.send.return_value = True        price = calc_discount(200, "svip")        notifier.send(price)        notifier.send.assert_called_once_with(160.0)  if __name__ == "__main__":    unittest.main(verbosity=2)
目的:用标准库 unittest 实现一个折扣计算器的完整测试,涵盖等价类划分、边界值分析、异常断言和 Mock 行为验证,跑一遍即可看到红灯绿灯的全过程。
  1. def calc_discount(price, level):被测函数,根据会员等级计算折后价,是测试的靶心。
  2. if price is None or price < 0:边界保护:空值和负数直接抛异常,这是无效等价类的处理入口。
  3. rate = {"normal": 1.0, "vip": 0.9, "svip": 0.8}[level]折扣率映射表,字典取值时若 key 不存在会自动抛 KeyError。
  4. def test_vip_gives_ten_percent_off(self):方法名以 test_ 开头,unittest 框架会自动发现并执行。
  5. self.assertEqual(calc_discount(100, "vip"), 90.0)断言函数返回的实际值等于期望值,是测试的“法官”。
  6. with self.assertRaises(ValueError):断言“应当抛异常”的场景,验证异常输入被优雅拒绝。
  7. notifier = Mock()创建替身对象,模拟真实的通知服务,不真的发消息。
  8. notifier.send.assert_called_once_with(160.0)行为验证:断言 send 方法恰好被调用一次,且参数是 160.0。

常见误区

误区所有单元测试都通过,说明这段代码是完美的,可以放心上线了。

正解测试全绿只代表“你测过的情况都符合预期”,不代表“没测过的情况没有问题”。老陈敲过十根木头,不代表第十一根不用敲。覆盖率之外还有断言质量、场景遗漏、边界盲区。更危险的是,如果断言写得太弱(比如只检查返回值不为空),即使每行代码都执行了,逻辑错误依然能溜过去。

为何常见:人天然倾向于把“绿灯”理解为“正确”的信号,却忽略了绿灯只说明“走过的路没塌”,没说“没走过的路安全”。

误区既然 Mock 能隔离依赖,那就把所有外部依赖全部 Mock 掉,测试完全脱离真实环境,多干净。

正解过度 Mock 会让测试变成“自说自话”——被测代码和替身都在你的掌控里,测试验证的是“你和自己预演的一致”,而不是“和真实世界的一致”。别忘记 Mock 只是“标准卯口样板”,样板再标准也不等于隔壁柱子真的装好了。数据库的 SQL 语法错误、第三方接口的字段变化,这些恰恰是 Mock 永远发现不了的。策略:外部依赖才 Mock,自己的核心逻辑代码尽量用真实实现。

为何常见:Mock 立竿见影地解决“环境不稳定、跑得慢”的痛点,容易让人产生“堵住一切外部因素”的掌控错觉。

误区覆盖率报告显示 100%,说明每一行代码都被测过了,这代码质量没得挑。

正解覆盖率只统计“执行过”,不统计“验证对”。一段代码被跑了一百遍,如果每次只断言“返回值不是 None”,那这段代码其实只被“参观”了,没有被“检验”。更隐蔽的是,100% 行覆盖率可能只走了 if 分支没走 else 分支——分支覆盖率才是衡量逻辑覆盖的硬指标。覆盖率是体检报告,指出盲区;断言质量才是医生的诊断能力,二者缺一不可。

为何常见:覆盖率是唯一能数字化展示的指标,人很容易把“可量化的指标”误当成“质量本身”,于是团队为了刷分而堆测试。

误区公司有测试团队和 QA,写单元测试是他们的职责,开发只要把功能写完就行。

正解单元测试测的是“最小代码单元的内部逻辑”,这件事只有写这段代码的人最清楚。测试工程师可以做测试策略、集成测试、自动化框架,但不可能比开发更懂某个函数应该返回什么。老陈的工地里,每根木头是采买的人自己验的,而不是等验收组来一根一根查——单元测试是开发者的基本功,是代码的“出厂质检”,不是别人替你兜底的借口。

为何常见:写测试确实比写功能“慢”,且有“额外工作量”的感觉,人容易把它归类到“不该我干”的活里,以此缓解拖延的愧疚。

面试题

Q1. 单元测试主要测试的对象是什么?

单选题 · easy

  • A.整个系统的端到端流程
  • B.单个函数或类的方法← 正确答案
  • C.页面 UI 的视觉样式
  • D.数据库的查询性能

答案:B。单元测试以最小代码单元(函数/方法)为对象,不启动整个系统。老陈敲的是“每一根木头”而不是“整栋房子”。端到端流程属于 E2E 测试,UI 视觉和数据库性能也都不在单元测试的射程内。

Q2. 测试金字塔中数量最多的测试是哪一层?

单选题 · easy

  • A.端到端的全流程测试
  • B.模块之间的集成测试
  • C.最小粒度的单元测试← 正确答案
  • D.手工执行的功能测试

答案:C。测试金字塔底层是单元测试,数量最多、跑得最快、成本最低。集成测试居中,端到端测试在塔尖数量最少。7:2:1 的比例提醒你:地基越厚,房子越稳。

Q3. 等价类划分的核心思想是什么?

单选题 · mid

  • A.穷举所有可能的输入组合
  • B.按行为分组每类取代表← 正确答案
  • C.只测试边界附近的取值
  • D.随机生成大量输入样本

答案:B。等价类划分把输入按“是否会产生相同行为”分组,每组只取一个代表值来测,用有限的用例覆盖无限的可能。穷举不可行,随机采样不系统,边界值分析是另一个独立方法而非等价类思想本身。

Q4. 针对取值范围 [0, 100] 的参数,边界值分析建议测哪些值?

单选题 · mid

  • A.只测 50 一个中间值
  • B.只测 0 和 100 两端
  • C.测试若干等间距的随机值
  • D.测越界和边界的内外点← 正确答案

答案:D。标准边界值集合是 min-1、min、min+1、max-1、max、max+1,即 -1、0、1、99、100、101。只测两端会漏掉“min+1 被误伤”这类 off-by-one bug,中间值和等距抽样都不是针对边界的策略。

Q5. 下列哪些场景适合使用 Mock 技术?

多选题 · mid

  • A.被测函数依赖外部数据库← 正确答案
  • B.被测函数调用第三方支付接口← 正确答案
  • C.被测函数内部只有一个纯计算
  • D.被测函数依赖系统当前时间← 正确答案

答案:ABD。外部数据库、第三方支付、当前时间都属于“不可控或不可重复”的依赖,适合用 Mock 变成固定行为。纯计算函数没有任何外部依赖,直接传真实参数测即可,引入 Mock 反而多此一举。

Q6. 某项目行覆盖率 100%,以下说法正确的是?

单选题 · hard

  • A.代码里绝对不存在隐藏的 bug
  • B.所有代码行都被执行过但不保证逻辑对← 正确答案
  • C.所有测试断言都必然非常有价值
  • D.以后不需要再补充任何测试用例

答案:B。覆盖率只统计“执行过”不统计“验证对”。100% 行覆盖可能只走了 if 没走 else,或断言形同虚设。老陈就算每根木头都敲了,敲的力度不对也发现不了内部裂纹。它是体检报告,不是免死金牌。

Q7. 下列哪项最能提升测试的可维护性?

单选题 · mid

  • A.测试断言写得越多越好
  • B.用一个大函数测完所有功能
  • C.一个测试只验证一个行为← 正确答案
  • D.测试命名随意只要跑得通

答案:C。一个测试只验证一个行为,失败时能精确定位到具体业务环节,这是可维护性的基石。断言堆砌和“万能大测试”看起来省事,失败时却让人无从下手。命名即文档,随便起名等于给未来的自己埋雷。

Q8. 以下哪些属于“好测试”的特征?

多选题 · hard

  • A.运行快速且结果稳定← 正确答案
  • B.依赖真实网络环境联调
  • C.只测被测对象的自身逻辑← 正确答案
  • D.失败时能定位到具体行为← 正确答案

答案:ACD。好测试跑得快、不抽风(无 flaky)、隔离外部依赖、失败时精确定位。依赖真实网络会让测试变慢且不稳定,违背单元测试的“隔离”原则——真实联调交给集成测试去做。

Q9. 测试驱动开发(TDD)的正确节奏是什么?

单选题 · easy

  • A.先写实现代码后补测试
  • B.先写测试见失败再写实现← 正确答案
  • C.先上线再补测试记录
  • D.只写测试代码不写实现

答案:B。TDD 的标准循环是 Red-Green-Refactor:先写一个必然失败的测试(红灯),写最简代码让它通过(绿灯),再重构保持全绿。先写实现再补测试容易“为了通过而测”,就失去了测试作为导航仪的意义。

Q10. 关于代码覆盖率,下列说法正确的是?

多选题 · mid

  • A.覆盖率越高代码就越简单易懂
  • B.行覆盖率无法保证分支全覆盖← 正确答案
  • C.覆盖率能反映测试的触碰范围← 正确答案
  • D.100% 覆盖率代表代码没有缺陷

答案:BC。行覆盖率只统计语句是否执行,if/else 只走一边也能到 100%,所以它无法保证分支全覆盖。覆盖率确实能反映“测试碰了多广”,但和代码复杂度、缺陷数量没有直接关系。100% 覆盖率更不能和“无缺陷”画等号。