前一阵子接手了内部代号ob的系统功能测试,从环境搭建到用例执行,再到问题记录和排查,整个过程差不多横跨了两周时间。这块负责的主流业务是数据流转和工单处理,听起来不复杂,但真跑起来之后发现,越普通的系统越能在细节处藏问题。写这篇记录的目的很简单,就是把我在ob测试过程中实际遇到、定位、解决并验证过的问题做个完整的复盘,包括问题现象、排查思路、日志分析和最终的修复验证。无论你是在做功能测试、接口测试,还是准备补一些并发场景的验证,这篇内容应该都能给你一些参考。
先说结论:测试这件事,最怕的不是发现问题,而是问题复现不了、定位不准,最后扯来扯去变成“环境问题”“偶发现象”。所以本篇会重点分享日志分析和问题定位的过程,这些才是排查问题的核心能力。
1. 测试环境与前期准备
1.1 被测系统与测试范围
ob系统是内部的一个工单数据管理平台,核心链路是用户创建工单、系统做数据校验、状态流转、最终归档。这次测试的重点放在三条主链路上:
- 工单创建到审核的完整流程
- 数据导入导出过程中的边界校验
- 多用户并发操作时的数据一致性
因为系统本身不是从头开发的项目,属于迭代优化版本,所以这次测试还兼顾了老功能回归。范围定清楚之后,和开发对齐过一次需求,确认了本轮没有涉及底层架构改动,主要是逻辑层优化和接口调整。这样一来,测试重心就可以放在功能正确性和数据一致性上。
1.2 环境搭建与数据准备
测试环境是我们自己搭的,用的是常规的“测试环境分离”方式。服务端版本是本轮迭代分支,数据库用了独立的测试实例,避免了和开发环境互相干扰。中间件这边有缓存、消息队列和文件存储服务,都需要提前确认版本和配置。
环境搭好后,有几个配置问题提前处理掉:
- 数据库连接池大小调到了和线上一致的参数,避免因为连接不足导致只能测出假性能问题
- 消息队列消费者的并发数单独配置,方便模拟并发场景
- 日志级别调到了DEBUG,方便排查问题时直接看调用链
测试数据准备这块,我一般不会直接用线上数据,而是根据用例需要分了几类构造数据:
- 正常合法数据,用于主流程验证
- 边界数据,比如字符长度刚到限制、数字取边界值
- 异常数据,比如缺字段、格式非法、重复提交
- 大数据量数据,用于导出功能和列表查询的验证
有一点要提醒,准备数据时最好顺手把数据造数脚本存下来,后面回归测试时还能复用。我这次就是在第一次造数之后没有做版本管理,后来环境重置过一次,数据全没了,重新造数花了不少时间。
2. 测试用例设计与执行计划
2.1 用例设计的基本思路
我看过很多测试新人设计的用例,最大的问题是“覆盖了正常流程就觉得完成了”。正常流程跑通只是最基础的要求,真正的用例设计功夫全在反向操作和边界条件上。这次ob项目的用例设计,我主要按四层来拆:
- 功能层:每个接口、每个状态流转节点的正常与异常场景
- 接口层:入参校验、返回码、异常码的覆盖
- 数据层:落库数据的正确性、状态一致性和幂等性
- 并发层:多用户同时操作同一单据、同一接口被重复调用
其中并发层的设计是最花时间的。像“同一个人同时提交两张相同内容的工单”、“不同人同时审核同一张工单”、“批量导入时又有人对同一批数据做修改”这类场景,都是实际很容易出现问题的地方,但常规用例里特别容易被忽略。
2.2 用例组织与优先级
用例拆分完毕之后,组织策略我习惯按优先级分成P0、P1、P2。P0是必须通过的主流程和核心链路,P1是重要的业务分支和异常场景,P2是边缘功能和体验类问题。
具体分布大概是这样的:
- P0用例:50条左右,覆盖工单创建、审核、状态流转、数据归档等主流程
- P1用例:80条左右,覆盖权限控制、重复提交、接口异常入参、数据边界
- P2用例:40条左右,覆盖前端展示、提示信息、列表排序和部分兼容性验证
执行顺序上,先把P0的全部跑完,确认主干没有问题之后,再跑P1和P2。这样做的原因是,如果主流程本身就是通的,后面发现的问题大概率就是分支逻辑或者细节处理问题,定位的范围可以快速缩小。
2.3 执行计划的动态调整
本来计划一天跑完P0,两天跑完P1和P2,实际执行中做了不少调整。原因很简单,有些用例一早发现依赖的接口返回数据不对,导致后续用例全部受影响,这种情况只能先暂停,等环境恢复后再继续。
测试执行计划不要排得太死,一定预留出缓冲时间。我在计划里都会留出20%到30%的时间用于环境修复、问题复测和回归。这次实际用下来的体验是,缓冲时间不仅能救急,还能在发现问题后有余力把排查做深,而不是草草记录一个bug就完事。
3. 测试过程中的典型问题记录与排查
这一章是整篇记录里含金量最高的一部分。我在ob测试过程中前前后后记录了几十个问题,但真正有代表性、值得展开讲的是下面这几类。每一个都经历过现象观察、初步猜测、日志定位、代码走查和最终验证的完整闭环。
3.1 并发场景下重复提交导致数据重复
问题现象:测试并发用例时,用脚本模拟两个用户同时提交相同内容的工单,正常预期是只有一个工单能创建成功,另一个被拦截或返回重复提示。但实际执行结果是两张工单都进了库,数据出现重复。
刚开始我以为是脚本问题,毕竟这种并发场景经常是脚本本身把参数写死导致看似并发。但把脚本仔细检查一遍之后,确认两个线程用的请求参数不同但业务值相同,请求确实同时到达了服务端。
接着看后端日志。我先查了数据库的插入记录时间,两条记录的创建时间只差了不到100毫秒,基本可以判断是同时进到写库逻辑的。然后去看工单编码生成这一块的日志,发现两次请求都成功生成了单号,说明问题不是简单的前端重复点击,而是后端在编码唯一性校验这个环节出现了竞态条件。
查到这里我基本能确定问题方向了:服务端判断“是否存在相同工单”的逻辑和“生成新工单”的逻辑不是一个原子操作,两个并发请求同时进到了判断逻辑里,都认为不存在相同数据,然后各自生成了一条新记录。
排查到这一步还没结束,我让开发帮忙确认了具体实现。果然是先查询再加一个唯一性校验,而工单编码字段没有加数据库层面的唯一索引,等于双重保障都没有。
问题定性之后,修复方案也比较明确:一是数据库工单编码字段增加唯一索引,从根源上挡住重复数据;二是业务逻辑里把创建动作改成带条件的形式,避免并发场景下同时通过校验。
这个问题的教训是,靠查询再插入这种check-then-act模式,在并发场景下就是典型的竞态条件,对测试来说,验证点不能只停留在功能是否通过,还要主动制造并发条件去试。
3.2 数据导出接口偶发超时
问题现象:导出大数据量工单数据时,第一次跑得很慢但能成功,第二次再跑直接超时,接口报504。单独查列表接口没有问题,分页查询也正常,只有导出这个链路会超时。
这个问题的排查过程比较曲折。最开始怀疑是接口本身的SQL执行太慢,就先去看了数据库慢查询日志,确实有两条耗时比较久的SQL。但慢SQL只是表象,优化之后再次导出,超时问题复现。
紧接着我把关注点放到接口的调用链上,发现导出功能并不是直接查库,而是先查一次统计数据,再异步生成导出文件,最后通过前端轮询拿结果。看似没有问题,但日志里显示在导出高峰期,生成文件这一步会长时间占用工作线程,导致后续请求排队。
继续深挖下去发现,问题出在工作线程池的核心线程数设置得太小,而且队列容量几乎没有,大量导出任务进来之后把资源占满,其他所有依赖该线程池的请求全部变慢,最终超时。换句话说,导出慢只是表象,真正的瓶颈是线程池的资源争抢。
这个问题的修复也不复杂,核心是给导出任务单独分配了一个线程池,核心线程数和最大线程数按导出场景单独调大,同时把队列改成有界队列,超过容量直接拒绝新任务并返回明确提示。再压测验证时,同一批数据导出三次,全部在预期时间内完成,不再拖垮其他接口。
这轮排查给我的感受很深:很多偶发超时问题根本不是单一原因,而是多个因素叠加导致的。只看一次日志很可能被表象带偏,同一问题要多复现几次,结合线程池、连接数等运行指标一起看,才能真正定位根因。
3.3 批量导入时的数据校验边界疏漏
问题现象:批量导入工单数据时,有一批数据中间某几条的某个字段长度超过了数据库字段定义的长度上限。按照需求文档,超长的数据应该返回明确的校验失败信息,并标记出具体是哪几行导入失败。但实际测试时,超长数据被静默截断了,导入结果里没有报错,数据入库后字段被切了一截。
这个问题属于典型的边界条件覆盖不足。单条创建的校验逻辑是正常工作的,因为单条提交时前端会先做一次长度校验,超长了直接弹提示。但批量导入走的是后端批量解析,前端的长度限制脚本在这个场景下根本不起作用。
排查时先对比了单条创建和批量导入的校验代码路径,发现批量导入的解析逻辑里对字段长度的校验被漏掉了,等于这个校验只存在于单条路径。数据入库时因为数据库在非严格模式下会自动截断超长内容,所以数据最终还是写进去了,只是内容被阉割。
这个问题如果不仔细验证,极容易漏掉,因为单条路径和批量路径入口不同,测试人员很容易只测了单条,忘了批量。后来我把所有“单条能用但不代表批量能用”的逻辑都梳理了一遍,针对导入导出场景增加了多组边界数据,比如字段最大长度、刚好超长一个字符、特殊字符、空值等情况,重新做了一遍完整验证。
提示:功能测试最怕的不是复杂的业务逻辑,而是同一条校验规则在不同入口下的实现不一致。凡是系统里存在“单条操作”和“批量操作”两套入口的,必须用例成对设计。
3.4 自动化脚本偶发误报
问题现象:在回归自动化脚本里,有一个“创建工单后列表页自动出现新单”的断言,跑了几轮之后开始偶发失败。人工去页面上验证时数据是正常的,导致我一度怀疑是脚本不稳定。
观察了几轮发现一个规律:脚本失败的时间点,都是页面刚加载完、列表数据还没刷新完成的瞬间。根本原因是断言执行得太快,前端列表还是空的,脚本就去找新单的数据模型,自然找不到。
这个问题在自动化测试里特别常见,属于典型的稳定性问题而不是功能Bug。修复方式是调整等待策略,从固定休眠改成显式等待,等列表接口的响应返回之后再执行断言。具体做法就是轮询页面元素的出现状态,加一个超时时间,而不是无脑sleep几秒。
调整完之后脚本连续跑了一周,没有再出现过误报。这里也提醒一下,自动化用例失败后,先不要急着提单,看失败截图和日志,确认到底是功能问题还是脚本问题,再决定下一步。之前就见过有人把偶发误报当成功能Bug提给开发,来回折腾浪费了不少时间。
3.5 审核状态下修改数据的权限漏洞
问题现象:正常情况下,工单提交审核之后,创建人不能再修改工单内容,只有审核人能看到详情并做审核操作。但在实际测试时,我用创建人的身份直接调接口修改工单详情,接口竟然返回了成功,数据库里的内容被改了。
这个问题是通过接口测试发现的,因为页面上按钮已经置灰了,前端操作根本走不到修改请求,但接口层没有做对应的状态校验。
查日志和代码之后发现,修改接口只校验了“当前用户是不是创建人”这个权限,但没有校验工单当前的状态。也就是说,只要你是创建人,不管工单是在草稿状态还是审核中,都能发起修改,审核状态下的修改限制只做在了前端。
这种问题的危害在于,审核人看到的数据和实际上库的数据不一致,如果审核人没有刷新页面,就可能在不知情的情况下审核了一个被篡改过的工单。
修复方案是后端修改逻辑增加状态机校验,只有草稿状态才允许创建人修改,审核中、已完成状态一律返回操作失败。类似的状态机校验问题在业务系统里非常容易出现,测试时除了验证正常流转路径,还要重点验证“不该操作的时候强行操作”的路径。
4. 问题分类与高频问题速查
4.1 问题分类统计
这一轮ob测试下来,记录的问题按照类型划分大致情况如下:
| 问题类型 | 数量 | 占比 | 典型特征 |
|---|---|---|---|
| 功能逻辑问题 | 14 | 38% | 校验缺失、状态流转异常、业务分支错误 |
| 并发/数据一致性 | 6 | 16% | 重复提交、竞态条件、缓存不一致 |
| 接口/权限问题 | 8 | 22% | 接口越权、参数校验不严、异常码不准确 |
| 性能/超时 | 4 | 11% | 线程池耗尽、慢SQL、连接池瓶颈 |
| 兼容性/体验类 | 5 | 13% | 不同浏览器展示差异、提示不友好、操作无反馈 |
从数据占比能看出,功能逻辑问题依然是发现问题的主力,但并发和数据一致性问题的数量比预想中高。这说明常规功能用例做得比较充分,反而暴露了开发在并发设计和状态校验上的薄弱点。
4.2 问题严重程度分布
按严重程度我习惯分成四档:
- 致命:数据丢失、主流程完全阻塞,这类问题必须立即修复
- 严重:主要功能可用但结果错误,比如审核被绕过、数据不一致
- 一般:功能可用但用户体验或分支逻辑有问题,比如提示不准确
- 轻微:展示层小问题,不影响核心功能
这一轮测下来的分布是致命0个、严重7个、一般18个、轻微12个。严重问题主要集中在接口权限校验和并发数据一致性上。倒是没有出现一上来就崩库、数据全丢这种致命问题,说明基础主流程相对稳定。
4.3 高频问题速查与对症方案
我自己整理了一个速查表,可以在下次测试或排查时直接参考:
| 现象 | 优先排查方向 | 常见根因 |
|---|---|---|
| 数据重复创建 | 唯一索引、并发校验逻辑 | check-then-act竞态 |
| 接口偶发超时 | 线程池状态、连接池配置 | 资源争抢、队列打满 |
| 批量数据和单条结果不一致 | 单条与批量代码路径差异 | 校验逻辑只覆盖单条入口 |
| 修改接口“不该能改时能改” | 状态机校验、后端权限校验 | 校验只做在前端 |
| 自动化脚本偶发失败 | 等待策略、断言时序 | 页面加载和断言竞争 |
| 导入数据被静默截断 | 数据库严格模式、字段长度校验 | 非严格模式自动截断 |
排查问题时最忌讳的是先想当然。我见过很多人一看到超时就怀疑慢SQL,一看到重复数据就怀疑前端重复点击,实际根因往往离现象有几层距离。正确的做法是先看日志和链路,再结合业务逻辑做推断,最后用最小化复现手段验证结论。
注意:最常见的排查错误就是只复现一次就开始下结论。同一个问题至少要复现三次,并且每次复现时主动收集日志、调用链和上下文数据,再做综合判断。
5. 回归验证与踩坑经验
5.1 回归验证策略
问题修复之后,回归验证不是把原来的用例重新跑一遍就完了。我这次采用的策略是“分三层回归”:
第一层,针对修复点做精准回归。每个被修复的问题,除了重新执行原来失败的那条用例,还补充了两到三条和修复点相关的相邻场景用例,确保修复没有只堵了眼前的口子。
第二层,做核心链路回归。把工单从创建到归档的主链路完整跑一遍,主要确认修复过程中没有影响已有的正常逻辑。
第三层,做自动化冒烟回归。把P0用例集通过自动化脚本快速跑一遍,作为整体健康度检查。如果这一层出现新的失败,基本可以断定修复引发了副作用。
这个策略看起来简单,但实际很有用。精准回归负责确认问题修复,链路回归负责确认业务完整,冒烟回归负责兜底整体质量。三层都过了,这个问题的修复才算真正验证完成。
5.2 测试过程中的关键教训
这一轮测试有几个教训我觉得值得写下来。
日志先行。遇到任何问题,第一时间去拿日志。不要靠猜,不要靠看代码猜,更不要直接问开发“你觉得是什么原因”。先自己收集完整证据链,再和开发讨论,沟通效率和准确率都会高很多。
环境差异要重视。测试环境和线上环境如果参数配置不一致,很多问题在测试环境是测不出来的。比如线程池大小、连接池上限、数据库严格模式,这些配置项不同,某些并发场景和边界场景的表现会完全不同。环境搭建阶段多花半小时核对配置,后面能省出好几个小时的排查时间。
造数脚本要版本化。一开始造好的测试数据脚本没有纳入统一管理,后来环境重置直接丢了。重新造数不仅麻烦,而且很难完全还原原来的数据状态。正确做法是把造数脚本和测试用例放在一起,统一纳入版本管理。
不要轻视“偶发”两个字。偶发问题背后几乎都有一个确定性原因,只是触发条件没有完全满足。遇到偶发问题就持续复现、持续收集信息,直到找到稳定的触发条件,这本身就是测试的核心价值之一。
5.3 后续工作建议
从这轮ob测试的情况来看,开发团队在并发设计和状态机校验这两块还有明显提升空间。后续如果继续做迭代,我建议把下面几个方向纳入常态化检查:
- 所有“查询后写入”“修改前判断状态”的操作统一排查一遍,能加数据库约束的就加约束,不能加的至少要在代码层保证原子性
- 增加接口层的自动化覆盖率,尤其是权限校验、参数校验这类适合做接口测试的场景,尽早发现问题,避免层层传递到页面层才暴露
- 批量导入导出场景作为专项测试项,每次迭代都跑一遍边界数据用例,防止类似校验缺失的问题再次引入
测试执行到后期,我自己的体会是,发现问题只是起点,真正有价值的是把问题背后的原因挖透。一个“偶发超时”如果只停留在提单层面,那它在下一次迭代仍然会出现;如果把线程池、队列、资源竞争的根因都理清了,再遇到类似问题就能快速定位。这也是写这篇文章的初衷,希望这些真实的排查过程记录,能给正在做类似系统的测试同学一些参考。