单元测试与 TDD

改完代码,怎么敢说没搞坏? 靠人肉点一遍又慢又容易漏;单元测试把「这段逻辑对不对」写成自动化用例,改完跑一遍,绿了就多一层底气。TDD(Test-Driven Development)更进一步:先写 failing 的测试(红)、再写最少代码让测试过(绿)、然后重构(重构),用测试驱动设计和防护回归。本章讲单元测试的目的与范围测试金字塔中的位置、断言、mock 与隔离TDD 的红-绿-重构循环,以及何时写测试、测试的可维护性

一、单元测试的目的与范围

单元测试是针对「最小可测单元」(函数、类的方法、模块)的自动化测试,用断言验证:给定输入,输出或副作用是否符合预期。目的包括:回归防护——改完跑测试,行为被破坏能立刻发现;文档与契约——测试即「这段代码该怎么用、在什么条件下有什么结果」的活文档;设计反馈——难测的代码往往耦合高、职责混,促使我们改进结构;信心——敢重构、敢改,因为有测试兜底。

范围:单元测试应快、稳定、隔离。测的是「这一块逻辑」,不依赖真实数据库、网络、文件(用 mock/stub 替代);不启动整个应用;不依赖执行顺序。这样单测数量可以很多、跑一次几秒内完成,适合每次提交都跑。

二、测试金字塔中的位置

测试金字塔把测试按「粒度」分层:底层单元测试最多——快、隔离、覆盖逻辑细节;中间集成测试较少——测模块/服务之间的协作、真实 DB 或 API;顶层端到端(E2E)测试最少——测完整用户流程、真实环境,慢且脆弱但能验证「整体能跑」。单元测试是底座:数量大、成本低、反馈快;集成和 E2E 补足「连起来对不对」和「用户场景是否通」。

测试金字塔:单元测试为底座,集成与 E2E 逐层减少
单元测试
多 · 快 · 隔离
集成测试
中 · 协作 · DB/API
E2E
少 · 全流程
三层占比示意

三、断言、Mock 与隔离

断言(Assertion)是测试里的「期望」:例如 expect(result).toBe(42)assert result == 42。测试执行被测代码,用断言检查返回值、状态或异常;断言失败即测试失败。写好断言要一个测试聚焦一个行为期望明确(不要一次 assert 十件事),失败时能立刻看出「哪里不对」。

Mock / Stub:被测单元若依赖数据库、网络、外部服务,单元测试里不真连——用假实现(stub)模拟对象(mock)替代。Stub 提供可控的返回值;Mock 还能验证「是否被调用、调用了几次、参数是什么」。这样测试只关心当前逻辑,不依赖外部、不慢、不 flaky。

隔离的含义
单元测试应独立于:真实 DB、真实 API、其他未测模块、时间/随机数等非确定性因素。通过依赖注入 + Mock/Stub,把「外部」换成可控的替身,保证测试结果只由被测代码和输入决定。
// 被测逻辑依赖 OrderRepository;单测里注入 Mock
const mockRepo = { findById: jest.fn().mockResolvedValue({ id: 1, total: 100 }) };
const service = new OrderService(mockRepo);
const result = await service.getOrderTotal(1);
expect(result).toBe(100);
expect(mockRepo.findById).toHaveBeenCalledWith(1);
示例:用 Mock 替代 Repository,断言返回值与调用

四、TDD 的红-绿-重构循环

TDD(Test-Driven Development)是一种写法:先写测试(红)——写一个尚未实现的用例,跑测试,看到失败;再写最少代码让测试过(绿)——不追求完美,只求通过;然后重构(重构)——在测试保护下整理代码。循环进行,功能一点点长出来,且始终有测试覆盖。

好处:需求被拆成「可测的小目标」;设计会自然偏向「可测」即低耦合;回归有保障。不必所有代码都 TDD,但核心逻辑、易错处、要长期维护的模块很适合用 TDD 或「先补测试再改」。

Red Green Refactor Red …
TDD 循环:写失败测试 → 写代码变绿 → 重构 → 下一轮
红-绿-重构三步骤

五、何时写测试、测试的可维护性

何时写:核心业务逻辑、易错分支、会被多次改动的代码优先写;一次性脚本、原型可酌情少写。TDD 是「先写测试再写实现」;若已有代码,可「先补测试再重构」——在要动的代码周围加测试,再放心改。

可维护性:测试也是代码,会坏、会变。建议:测试命名表意——如 should_return_discount_when_order_over_100,失败时一眼知道哪个场景挂了;一个测试一个行为——不要一个用例测十件事;少依赖实现细节——测行为与契约,不测「内部调了哪个私有方法」;避免重复——用 setUp、fixture、工厂函数抽公共数据,但别让测试读起来像谜题。测试若难读、难改,大家就不愿维护,会逐渐被关掉或删掉。

一句话: 单元测试用断言验证最小可测单元,要快、稳、隔离,在测试金字塔里是底座。用Mock/Stub替代外部依赖实现隔离。TDD 是红(写失败测试)→ 绿(最少代码过)→ 重构的循环。何时写:核心逻辑、易错处、要长期改动的优先;测试要可维护:命名清晰、一测一行为、少依赖实现。

小贴士: 测试失败时,先看失败信息能否直接指向「期望什么、实际什么」;若只能看到「某行报错」,说明断言或用例设计可以改进,让下一次失败更容易定位。

六、小结

单元测试目的:回归防护、活文档、设计反馈、重构信心;范围要快、稳、隔离。测试金字塔:单元多、集成中、E2E 少。断言表达期望;Mock/Stub 替代依赖实现隔离。TDD:红-绿-重构循环,先测后实现。何时写:核心与易错优先;可维护性:好命名、一测一行为、少依赖实现。下一章讲代码评审与协作,把「怎么通过 Review 保证质量与传播知识」说细。