单元测试与 TDD
一、单元测试的目的与范围
单元测试是针对「最小可测单元」(函数、类的方法、模块)的自动化测试,用断言验证:给定输入,输出或副作用是否符合预期。目的包括:回归防护——改完跑测试,行为被破坏能立刻发现;文档与契约——测试即「这段代码该怎么用、在什么条件下有什么结果」的活文档;设计反馈——难测的代码往往耦合高、职责混,促使我们改进结构;信心——敢重构、敢改,因为有测试兜底。
范围:单元测试应快、稳定、隔离。测的是「这一块逻辑」,不依赖真实数据库、网络、文件(用 mock/stub 替代);不启动整个应用;不依赖执行顺序。这样单测数量可以很多、跑一次几秒内完成,适合每次提交都跑。
二、测试金字塔中的位置
测试金字塔把测试按「粒度」分层:底层单元测试最多——快、隔离、覆盖逻辑细节;中间集成测试较少——测模块/服务之间的协作、真实 DB 或 API;顶层端到端(E2E)测试最少——测完整用户流程、真实环境,慢且脆弱但能验证「整体能跑」。单元测试是底座:数量大、成本低、反馈快;集成和 E2E 补足「连起来对不对」和「用户场景是否通」。
多 · 快 · 隔离
中 · 协作 · DB/API
少 · 全流程
三、断言、Mock 与隔离
断言(Assertion)是测试里的「期望」:例如 expect(result).toBe(42)、assert result == 42。测试执行被测代码,用断言检查返回值、状态或异常;断言失败即测试失败。写好断言要一个测试聚焦一个行为、期望明确(不要一次 assert 十件事),失败时能立刻看出「哪里不对」。
Mock / Stub:被测单元若依赖数据库、网络、外部服务,单元测试里不真连——用假实现(stub)或模拟对象(mock)替代。Stub 提供可控的返回值;Mock 还能验证「是否被调用、调用了几次、参数是什么」。这样测试只关心当前逻辑,不依赖外部、不慢、不 flaky。
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);
四、TDD 的红-绿-重构循环
TDD(Test-Driven Development)是一种写法:先写测试(红)——写一个尚未实现的用例,跑测试,看到失败;再写最少代码让测试过(绿)——不追求完美,只求通过;然后重构(重构)——在测试保护下整理代码。循环进行,功能一点点长出来,且始终有测试覆盖。
好处:需求被拆成「可测的小目标」;设计会自然偏向「可测」即低耦合;回归有保障。不必所有代码都 TDD,但核心逻辑、易错处、要长期维护的模块很适合用 TDD 或「先补测试再改」。
五、何时写测试、测试的可维护性
何时写:核心业务逻辑、易错分支、会被多次改动的代码优先写;一次性脚本、原型可酌情少写。TDD 是「先写测试再写实现」;若已有代码,可「先补测试再重构」——在要动的代码周围加测试,再放心改。
可维护性:测试也是代码,会坏、会变。建议:测试命名表意——如 should_return_discount_when_order_over_100,失败时一眼知道哪个场景挂了;一个测试一个行为——不要一个用例测十件事;少依赖实现细节——测行为与契约,不测「内部调了哪个私有方法」;避免重复——用 setUp、fixture、工厂函数抽公共数据,但别让测试读起来像谜题。测试若难读、难改,大家就不愿维护,会逐渐被关掉或删掉。
一句话: 单元测试用断言验证最小可测单元,要快、稳、隔离,在测试金字塔里是底座。用Mock/Stub替代外部依赖实现隔离。TDD 是红(写失败测试)→ 绿(最少代码过)→ 重构的循环。何时写:核心逻辑、易错处、要长期改动的优先;测试要可维护:命名清晰、一测一行为、少依赖实现。
六、小结
单元测试目的:回归防护、活文档、设计反馈、重构信心;范围要快、稳、隔离。测试金字塔:单元多、集成中、E2E 少。断言表达期望;Mock/Stub 替代依赖实现隔离。TDD:红-绿-重构循环,先测后实现。何时写:核心与易错优先;可维护性:好命名、一测一行为、少依赖实现。下一章讲代码评审与协作,把「怎么通过 Review 保证质量与传播知识」说细。