
1. 编码环节从“能跑”到“能维护”1.1 编码规范为什么不是形式主义教科书在讲编码的时候通常会把“编码风格”“命名规范”“注释规范”这些内容放在最开始很多同学觉得这部分就是“排版要求”可有可无。但真到了项目里编码规范直接决定了这个项目能走多远。我见过太多“能跑就行”的代码跑起来确实没问题但三个月后需求一变没人愿意去改因为改一个方法要顺藤摸瓜翻七八个文件变量名全是a、b、data、temp没有注释逻辑全靠猜。最后只能推倒重写。这其实就是编码阶段偷懒的代价。软件工程里的编码不只是“把需求翻译成代码”更是“把代码写得让别人能看懂、能维护”。从工程角度讲编码规范至少解决三件事降低沟通成本。团队里每个人的习惯不同如果没有统一规范你写的缩进和他写的换行都不一样合并代码时空格差异就能引发几十处冲突纯粹浪费时间。减少低级错误。比如命名规范里要求“局部变量首字母小写、常量全大写”这不只是好看而是让读代码的人一眼就能判断变量的作用域和是否可修改避免误用。便于代码审查。好的规范让审查者能快速定位问题不用把时间耗费在“这段代码在干什么”上而是聚焦“这段代码对不对、好不好”。有人会问个人项目也要搞规范吗我的建议是哪怕只有自己看也值得养成习惯。这跟你一个人住要不要整理房间是一个道理——整理不是为了给别人看而是为了自己找东西的时候不抓狂。我现在接手任何一个新项目第一件事就是看它的命名风格和目录结构基本就能判断这个项目的健康程度。1.2 代码审查给别人挑错也被别人挑错代码审查Code Review是编码环节里最容易忽视、但回报率最高的一步。说白了就是写完代码不要急着提交合并让团队里其他人看一遍。别小看这个过程很多隐蔽的问题就是在这一步被拦下来的。审查主要看几个层面逻辑正确性这段代码在不同输入下是不是都能得到正确结果有没有遗漏边界条件可读性换一个人来看能不能在十分钟内看懂思路需不需要反复追问安全性有没有潜在的崩溃风险数据会不会被意外修改有没有资源泄漏可扩展性下次需求变化的时候这段代码是容易调整还是要推翻重写我在实际项目里遇到过最典型的例子一个同事写了一个批量导入功能测试用例全过但在代码审查时发现如果导入的文件里有一行格式错乱整个程序会直接崩溃而不是跳过这一行并继续处理剩余数据。这个bug靠功能测试很难触发因为测试数据往往都是工工整整的但真实用户上传的文件千奇百怪最终这个解决方案是在审查阶段提出并修正的避免了上线后的重大事故。代码审查也是新手成长最快的方式之一。你写代码的时候觉得自己思路天衣无缝但被同事问一句“这个状态如果重复触发会怎样”往往就能发现自己漏掉的场景。反过来帮别人审查代码也能学到很多自己不熟悉的写法和技巧。特别是刚入行的同学我强烈建议主动申请参与代码审查哪怕只是旁听收获也远大于埋头自己写。1.3 编码完成不等于任务完成集成环节的认知很多初学者有一个误解认为“代码写完、能编译通过、功能没问题”就算完成了。但在软件工程里编码完成只是第一步紧接着的集成往往是真正的战场。集成指的是把各个模块组合到一起让整个系统跑起来。难点在于每个模块单独测试都没问题不代表合在一起就没问题。接口参数不匹配、数据格式不一致、调用顺序依赖、并发冲突——这些问题只有在集成时才会暴露。举一个我曾经踩过的坑一个项目里有三个小组分别开发订单模块、库存模块和支付模块各自测试都通过了。集成联调的那天订单模块调用库存模块的接口发现库存服务返回的是JSON格式但订单模块期望的是XML支付模块回调订单模块时因为超时设置太短导致商城内部分订单在支付成功后被系统误判为失败又自动发起了退款。这些问题的根源不在单个模块内部而在于模块之间的约定没有拉齐。所以现在很多团队都在推行“持续集成”也就是代码提交后自动编译、自动跑测试、自动部署到测试环境。目的就是在每次代码变动时尽早发现集成问题而不是等到最后一天合并所有代码一次性面对一堆无法定位的错误。这个过程在教科书里只是简单提了几句但它在真实研发流程里占据的时间往往是编码本身的好几倍。2. 测试的层次与设计思路2.1 测试金字塔从底层到顶层的策略软件工程里讲到测试绕不开一个概念叫“测试金字塔”。它把测试分成三个层次底层是单元测试数量最多中间是集成测试顶层是端到端测试数量最少。为什么是金字塔形状因为越底层的测试运行越快、执行成本越低、定位问题越精准所以要大量覆盖越顶层的测试虽然更接近真实用户场景但运行慢、环境依赖重、出了问题也不好定位所以只能重点覆盖核心流程。单元测试针对的是“一个函数或一个类”的行为验证它的输入输出是否符合预期。这就像检查一台机器里的每个零件——螺丝拧好了没有、齿轮转不转得动。集成测试针对的是“多个模块协作”的场景例如用户下单后库存减扣和订单状态更新是否能正确联动。端到端测试则模拟真实用户从打开页面到完成操作的完整流程相当于整条流水线跑一遍。很多团队的误区是一上来就死磕端到端测试总想着“测试要模拟真实用户”结果写了一堆又慢又脆弱的用例网络一抖就失败、界面多一个按钮就全红、跑一次要二十分钟。最后大家都懒得跑了测试沦为摆设。正确的思路是把70%的精力放在单元测试和集成测试上端到端只覆盖最核心的冒烟流程。2.2 测试用例设计的三板斧等价类、边界值、判定表写测试用例是最能体现“工程思维”的环节。不是把所有可能的输入都测一遍——那是不可能的而是用最少的用例覆盖尽可能多的场景。教科书里常用的方法有三种我到现在都还在用。等价类划分把输入数据按“程序处理方式是否相同”分成若干类从每个类里取一个代表值测试即可。比如一个模块要求输入“1到100之间的整数”输入数据可以分为三类合法整数1到100、小于1的数、大于100的数。合法整数这个类里你测10和测77本质上是一样的那就不需要把每个数都测一遍。边界值分析是等价类的补充。很多bug就藏在边界的左右两侧因为程序员写判断条件时最容易大意的地方就是“大于”和“大于等于”只差一个等号。测试1、100这两个合法边界的邻值——0、101——是必须的有时候还要测一下负数和0这样容易被忽略的特殊输入。判定表适合处理“多个条件组合决定结果”的逻辑。比如某个优惠活动规则是“满100减20且仅限新用户且非生鲜品类”这三个条件组合下来有八种情况判定表可以帮你逐条列出确保没有遗漏。我早年在电商系统里就靠判定表发现了两个条件组合里隐藏的问题非新用户买生鲜满200本该不打折但程序误判成打折了。这种问题靠随机测试很难碰到但如果按判定表去设计用例一查一个准。这三种方法不是独立的实际工作中往往组合使用。先划等价类确定大致范围再用边界值补边界最后用判定表梳理条件组合。一套组合拳下来用例的质量会比“随便点点”高出几个量级。2.3 覆盖率数字好看不是目的覆盖率是衡量测试完整度的重要指标常见的有行覆盖率、分支覆盖率、条件覆盖率等。行覆盖率指的是被执行的代码行数占总代码行数的比例分支覆盖率指的是条件判断的真假分支是否都被执行过。覆盖率当然越高越好但要清楚一个前提覆盖率只告诉你“哪些代码被执行了”并不告诉你“这些代码被验证得是否正确”。换句话说覆盖率是必要不充分条件。我遇到过有团队为了凑覆盖率专门写一堆“跑一下但是不做断言”的用例用工具一统计覆盖率90%以上上报给领导很好看但实际上啥也没验证。这就有点自欺欺人了。正确的姿势是先保证断言质量再追求覆盖率。断言才是测试的灵魂——你说一个函数“返回结果正常”到底“正常”是什么标准断言就是把标准写下来程序自动去比对。对于新项目我一般建议单元测试覆盖率的目标定为行覆盖率80%以上、分支覆盖率70%以上核心模块比如支付、订单、登录可以适当提高而那些只是包装调用、没有复杂逻辑的胶水代码不必强求覆盖率。3. 自动化测试与常见误区3.1 从手工测试到自动化测试如何迈出第一步手工测试是“人肉点来点去”自动化测试是“写代码让代码去测代码”。很多初学者一听到自动化测试就觉得很高级觉得要学一堆框架。其实第一步远没有想象的那么复杂。我建议从项目里最稳定、最频繁回归的功能模块开始。拿一个登录功能举例手工测试每次改完代码都要输入用户名密码、点登录、看跳转结果重复次数多了既枯燥又容易漏。自动化要做的事情就是把这一套动作用代码固化下来打开页面、输入数据、点击按钮、断言跳转结果与用户信息。以Python生态为例接口测试可以用pytest加requests库UI自动化可以用Selenium或Playwright。起步阶段不需要搭建多复杂的框架先写一个脚本能自动跑通核心流程就已经比纯手工前进一大步了。等你发现脚本越来越多、维护成本变高的时候再考虑引入测试框架、数据驱动、关键字驱动这些进阶手段。3.2 测试稳定性最容易被低估的敌人自动化测试跑起来之后最让人头疼的不是“测试发现了bug”而是“测试没发现bug但自己挂了”。用例失败的原因不是被测系统出问题而是环境因素测试数据被上一次运行污染了、某个操作需要等待5秒但脚本只等了3秒、本地网络慢导致页面没加载完就断言了。这种“测试不稳定”如果处理不好一定会被团队慢慢抛弃。人都是有惰性的——每次跑测试都有两个莫名其妙的报错查了半小时发现是环境问题谁还愿意天天跑解决不稳定问题有两条基本原则保证测试独立性。每个测试用例执行前都应该尽量准备好自己的数据不要依赖其他用例的执行顺序和结果。显式等待代替固定等待。比如UI测试里“等页面元素出现”要用轮询机制而不是一味地sleep。固定时间等待看起来简单但运行环境一变它就变成了失败率最高的来源。我早期写过一批单测本地全部通过一上CI持续集成就有几个失败。排查到最后发现测试用例之间共享了同一个临时数据库运行顺序不同数据状态就不同导致用例结果不可预期。后来改成每个用例跑前自动重建、跑后自动清理这个问题才彻底解决。3.3 测试数据管理为可重复执行打底测试数据是自动化测试里的隐形工程。没有一套稳定可控的数据管理策略测试跑得越多数据污染就越严重到最后测试环境里的数据长得像生产环境一样“丰富”反而无法预判结果。常用的策略有三种数据独立准备每个用例在执行前通过API或者SQL初始化自己需要的数据执行后清理。适合关键业务链路。数据复用池准备一批固定不变的基础数据比如测试账号、商品信息多个用例共用。适合那些本身不修改数据的只读场景。数据随机生成每次运行时生成全新的数据比如随机手机号、随机邮箱避免重复冲突。适合注册、创建订单这类会落库的场景。看起来都是细节但细节决定成败。一套能稳定重复跑的自动化测试背后一定有一套严谨的测试数据设计方案。4. 从理论到实践一个小型模块的编码与测试流程4.1 示例模块订单金额计算的核心逻辑聊了这么多理论用一个实际例子把整个流程串起来。假设我们要实现一个订单金额计算模块需求如下订单包含多个商品每个商品有单价和数量。订单满100元减20元。新用户额外打95折。以上优惠可叠加。这个需求看起来简单但里面藏着不少边界条件。我们按“先设计测试用例再写代码”的思路来做——这也是测试驱动开发的简单入门方式先用测试把“正确”的定义写清楚再让代码去满足这些定义。4.2 测试用例先写用例再编码根据需求我列出如下核心测试用例用例编号场景描述输入预期输出T01普通用户商品总额不满100元总额80元非新用户80元T02普通用户商品总额刚好100元总额100元非新用户80元T03普通用户商品总额超过100元总额150元非新用户130元T04新用户商品总额不满100元总额80元新用户76元T05新用户商品总额刚好100元总额100元新用户76元T06新用户商品总额超过100元总额150元新用户123.5元T07空订单空列表0元T08商品单价或数量为负单价-5数量2抛出异常为什么要先写用例因为用例就是需求的数字化表达。代码还没写我们已经把“正确”的标准定下了。后面编码时每一个用例就是一个验收标准跑通一个就完成一块这比把所有功能写完再回头测试要高效得多因为你能精确地知道哪里对了、哪里还差着。4.3 编码实现与测试代码的配合先写一个最初版本的实现逻辑。假设我们用Java来描述public class OrderCalculator { public double calculate(ListItem items, boolean isNewUser) { if (items null || items.isEmpty()) { return 0; } double total 0; for (Item item : items) { if (item.getPrice() 0 || item.getQuantity() 0) { throw new IllegalArgumentException(单价和数量不能为负); } total item.getPrice() * item.getQuantity(); } if (total 100) { total - 20; } if (isNewUser) { total * 0.95; } return total; } }然后写对应的单元测试。我习惯用JUnit 5来写每个用例对应一个测试方法方法名尽量描述清楚场景方便别人看懂失败时是哪个场景出了问题。Test void testNormalUserTotalBelow100() { OrderCalculator calculator new OrderCalculator(); ListItem items Arrays.asList( new Item(30, 2), // 总价60 new Item(20, 1) // 总价20 ); double result calculator.calculate(items, false); assertEquals(80, result, 0.001); } Test void testNewUserTotalAbove100() { OrderCalculator calculator new OrderCalculator(); ListItem items Arrays.asList( new Item(50, 3) // 总价150 ); double result calculator.calculate(items, true); assertEquals(123.5, result, 0.001); } Test void testTotalExactly100() { OrderCalculator calculator new OrderCalculator(); ListItem items Arrays.asList( new Item(25, 4) // 总价100 ); double result calculator.calculate(items, false); assertEquals(80, result, 0.001); } Test void testEmptyOrder() { OrderCalculator calculator new OrderCalculator(); ListItem items Collections.emptyList(); double result calculator.calculate(items, false); assertEquals(0, result, 0.001); } Test void testNegativePriceThrowsException() { OrderCalculator calculator new OrderCalculator(); ListItem items Arrays.asList( new Item(-5, 2) ); assertThrows(IllegalArgumentException.class, () - calculator.calculate(items, false)); }建议仔细看一下第7个用例即“空订单”的处理。需求里没有明确说明空订单应该返回什么但真实场景里接口完全可能收到空列表。在设计测试用例时主动思考这些需求没有覆盖到的情况恰恰是测试人员与普通用户最大的区别。还需要特别注意的是测试里用到了三个典型值——80、100、150——分别对应“不满100”“刚好100”“超过100”三种情况这就是前面说的边界值分析在实战中的体现。特别是“刚好100”这个值很容易在实现时写成 total 100导致等于100时折扣不生效还好我们通过边界值用例把它拦住了。4.4 可测试性设计写代码时为测试铺路很多人写代码的时候完全不考虑“这段代码怎么测”结果写出来一个几千行的巨型方法输入输出泾渭不明测试根本无从下手。这就涉及一个很重要的工程概念可测试性。让代码易于测试一般有几个方法函数尽量小而单一。一个函数只做一件事输入输出明确测试就很好写。避免大量静态方法和全局变量。它们会让测试之间相互影响也很难做隔离。依赖通过参数传入而不是函数内部直接创建。比如一个支付服务依赖外部接口把这个外部接口作为参数传进来测试时就可以传入一个假的实现而不用真的去调外部服务。在设计订单计算模块时如果把促销规则写死在计算函数里后面每次规则变化都要改函数、改测试但如果把规则抽成独立策略就能针对每种规则写单独测试。思路转变带来的提升可不是一点半点。5. 常见问题与排查技巧实录5.1 测试跑不过的常见原因速查表日常开发中测试失败的原因五花八门但其中大部分都可以归纳到几个固定类型。我把这些年遇到的情况整理成速查表排查优先级可以按这个顺序来。常见原因典型表现排查方法解决思路测试数据被污染单条用例单独跑通过放一起跑失败按顺序逐条运行观察失败是否与执行顺序相关用例前后重置数据避免共享态时间等待不足UI测试偶发超时重跑又能过看失败截图元素是否未出现在页面上改用显式等待轮询判断元素状态环境配置差异本地通过CI失败对比本地与CI的环境变量、依赖版本、数据库结构统一环境配置用容器或配置文件固化依赖断言太严格浮点数计算结果不相等打印实际值与期望值观察差异大小根据计算精度设置误差范围并发执行冲突测试并行跑时数据互相覆盖查看同一时间点执行的用例列表数据隔离或串行化相关用例需求理解错误代码符合文档但不符合真实场景与产品确认行为预期更新需求文档修正代码与用例这张表我会贴在工作台旁边每次测试挂掉先按表逐条排除而不是盲目地改代码或改测试能省下很多时间。5.2 测试代码的维护不只是被测试代码的事测试代码也是代码同样要维护、要重构、要评审。很多团队的新代码没人愿意写测试很大一部分原因是已有的测试代码写得太烂——几千行的测试类、充满重复逻辑的辅助方法、失败信息完全没有提示的断言谁看了都不想碰。我自己维护测试代码有一个原则如果测试代码的复杂度已经接近甚至超过被测代码的复杂度那就该停下来重构了。测试的职责是帮助理解系统行为如果它本身还需要花费大量精力才能理解那它就没有起到应有的作用。几个实用的维护手段公共的测试数据构造方法封装成工厂类避免每个用例里都写一遍长长的对象初始化代码。断言信息要写清楚。比如 assertEquals(80, result, 0.001)失败时只显示“expected: 80 but was: 75”并不直观。如果加上业务上下文信息就能更快定位问题。及时清理废弃的用例。业务逻辑变了旧的测试用例如果不再有效要么更新要么删除千万不能留着“反正跑不过先注释掉”。注释掉的测试代码就是技术债会在未来给你挖坑。5.3 测试先行还是后补一个折中的方案关于“要不要测试先行”业界争论了好多年。支持测试驱动开发的人认为“先写测试再写实现”能保证代码的可测试性反对的人觉得这会降低开发速度尤其是需求不明确时测试根本无从写起。按我的项目经验比较务实的做法是需求清晰的场景比如算法模块、工具类、接口协议采用测试先行的方法非常合适。反正需求就在那里先写用例能把需求落地得更准确。需求模糊的产品功能比如前端UI交互可以先写一小段探索性代码理清思路后再补测试。最忌讳的是上线后完全不补测试。如果一个核心功能模块上线后还没有任何自动化测试覆盖后面每次改动都是在走钢丝。我在实践中还有一种折中的写法先写“大致的测试”再写实现让测试从失败到通过最后再补上遗漏的边界用例。这样既保证测试的驱动力又不会因为一开始就追求完美的用例设计而卡住思路。6. 编码与测试之外工程化思维带来的额外收益编码与测试在教科书里是两个独立的章节但在真实项目里它们始终是一套组合拳。写代码不是把代码写完就扔给测试人员而是要把可测性刻进编码习惯里写测试也不是验证“这段代码对不对”而是为后续每一次重构和迭代提供安全网。我自己的体会是自从养成了“先想测试再写代码”的习惯编码的返工率明显下降。因为测试用例逼着你去思考边界条件、异常输入和需求的隐藏逻辑这比闷头写代码再回头调试高效得多。这里也想给在校生或者刚入行的朋友一个建议千万不要因为这门课是理论课就忽略它。软件工程里讲的编码规范、测试方法、集成策略每一节都是用大量失败项目换来的经验总结。你现在在课程设计里认真按规范写代码、认真写测试用例把这些习惯内化了等进了公司会省去很多被批改代码的尴尬。反过来如果现在糊弄过去这些坑迟早会在痛的工作中重新踩一遍。关于测试还有一个很多人不知道的小技巧当一个bug反复出现且找不到规律时把怀疑点从“代码逻辑”转移到“数据状态”。大多数看似不可能复现的问题深挖下去都是因为某条数据处于一种异常状态——比如数据库里有一条total为负数的订单或者某个用户资料里的字段值是NULL。编码与测试的边界其实不像课本里画的那么清晰真正到了实战中你往往既要写代码也要设计数据还要自己跑测试所有技能搭配在一起才是完整的工程能力。这一章的知识密度在软件工程课程里不算最高但它的应用频率绝对是最高的。只要你还在写代码编码与测试就是你每天都要面对的两件事。把这两件事做扎实你就已经跑赢了很多“能跑就行”的选手。