1. 测试对象与整体思路拆解1.1 考问通系统的业务链路到底长什么样拿到“考问通系统测试分析报告”这个任务时我第一件事不是打开用例管理工具而是先把这条业务链路在白板上画了一遍。原因很简单这类在线考试与问答测评系统单点功能的正确性从来不是最大风险链路断在哪个环节才是。考问通系统在我这里的定位是一套面向教学与培训场景的在线测评平台核心能力包括题库维护、组卷排考、考生作答、自动与人工判分、成绩发布与复核归档。它解决的问题是把线下纸笔测评搬到线上并且让出题、组卷、监考、阅卷、统计这几件事在同一套数据上流转不用反复导表对数。适合读这份分析的人有三类一是刚接手类似项目、需要一份可复用的测试拆解思路的测试同学二是负责系统交付、想知道验收该盯哪些点的研发和产品三是需要向上汇报测试结论、但手里只有零散执行记录、不知道怎么组织成报告的项目负责人。这三类人的关注点不一样但底层需求是同一个——想知道这套系统在真实考试压力下会不会出事出事会在哪里出。所以我把整条链路拆成十段题库入库、题目审核、组卷策略、考试发布、考生身份校验、作答与暂存、交卷与超时处理、判分核算、成绩发布、成绩复核与归档。每一段的失败成本完全不同。题库里错一道题影响的是所有抽到它的考生组卷策略出错可能整场考试的难度分布全部失衡而作答环节的暂存丢失往往只影响个别考生但投诉率最高。测试资源的分配必须按失败成本来而不是按功能模块的数量来平均切。这里有个容易被忽略的点考问通系统里“考试”是一个有时效性的实体它有开始时间、结束时间、交卷方式到时自动交卷还是手动交卷、允许迟到时长、允许中断次数。这些时间相关的配置项之间是有交互的两两组合起来的场景数量远超直觉。我通常会把这类时间配置单独拉一张表做正交组合而不是混在普通功能用例里否则很容易漏掉“迟到15分钟入场 中途断线3分钟 距结束2分钟手动交卷”这种叠加场景。1.2 为什么把测试重心从“功能点”挪到“链路优先”早期我做过一版按功能模块划分的测试计划题库、组卷、考试、判分、成绩五大块每块独立写用例独立验收。执行下来通过率很好看接近百分之九十八但上线后第一场真实考试就出了问题考生在考试中途切换了一下网络环境页面刷新后答题记录回退了三四道题。这个问题单看任何一个模块都测不出来——前端认为本地缓存还在后端认为最近一次提交是十秒前中间那几次异步提交在弱网下被合并或者丢弃了没有任何一方觉得是自己的错。那次之后我改了策略把用例按链路组织。每条链路是一个完整业务目标的端到端路径比如“一名迟到考生通过手机端完成一场90分钟考试的完整过程”里面串起了登录、身份校验、取卷、作答、暂存、断线重连、交卷、判分、出分九个环节。链路用例的数量比功能用例少得多但能挖出的缺陷质量完全不在一个层级。功能用例保证的是“这个按钮点了有反应”链路用例保证的是“这个人考完试能拿到分”。这个转变背后有个判断依据考问通系统属于典型的有状态长事务系统。一场考试对单个考生来说是一个持续几十分钟到几个小时的事务中间会经历几十次网络请求任何一次请求的时序错乱都可能导致状态不一致。传统按模块切的测试方法天然假设状态是干净的而真实考试里状态永远是脏的。所以我给自己定的规矩是链路用例的优先级永远高于功能用例时间不够的时候先砍功能用例。还有一层考虑是团队协作成本。链路用例天然是跨模块的写的时候必须把前端、后端、数据库三方的人拉到一起对齐这个过程本身就能提前暴露接口约定上的分歧。我见过太多项目前后端各自对着文档开发联调时才发现对“交卷成功”的定义不一样——前端认为接口返回成功就算交卷后端认为要等判分任务入队成功才算。这种分歧在功能测试阶段几乎发现不了在链路测试里一测就现原形。1.3 测试环境与数据基线怎么定环境这件事我必须多说两句因为考问通系统的测试环境如果建得随意后面所有结论都不可信。我一般分四级开发自测环境、集成测试环境、预发环境、生产影子环境。前两级可以共用数据库第三级必须数据隔离第四级只读生产数据做验证。预发环境的关键要求是配置与生产一致尤其是考试时长、自动交卷宽限时间、判分任务的重试次数这些参数测试环境和生产环境差一个值压测出来的结论就废了。数据基线我习惯在测试开始前锁定一份写成配置表固下来。下面这张表是我在考问通系统项目里实际用过的一份基线数值是按中等规模教学场景估的可以按实际业务量等比放大。基线项取值设定理由题库总题量20000 道覆盖多学科多题型模拟两年积累量单场考试考生数3000 人按单院系集中排考的上限估单套试卷题量80 题含单选、多选、判断、填空、简答五类主观题占比15%主观题决定人工阅卷压力考试时长90 分钟常规课程考试时长平均答题间隔60 秒/题用于推算请求频率同时进行中的考试场次3 场模拟排考叠加判分任务单场耗时上限120 秒自动判分必须在此时间内出分这份基线的作用是让所有测试结论有参照物。比如压测报告里写“系统支持 3000 并发”必须说清楚这个 3000 对应的题量、题型分布、答题间隔是多少脱离基线谈并发数字没有任何意义。我在评审别人的测试报告时第一眼看的就是有没有这张基线表没有的话后面的数据基本只能当参考。还有一点是数据造数的成本控制。题库数据不能全靠人工录我一般写脚本批量生成但生成的题目必须满足业务约束题干长度分布合理、选项数量符合题型要求、知识点标签不能全挤在同一个分类下、难度系数要接近正态分布。如果造出来的数据全是短题干、四选项、同一知识点那组卷算法里的去重逻辑和知识点覆盖逻辑就永远测不到边界。数据造得好一半的缺陷在造数阶段就能暴露。2. 核心细节解析与实操要点2.1 题库与组卷最容易埋雷的地方组卷是考问通系统里逻辑密度最高的一个模块也是最容易被低估的。表面上看就是“按规则抽题凑成一张卷子”实际上要同时满足一堆互相牵制的约束总分必须等于设定分值、题型配比必须符合模板、知识点覆盖度必须达到阈值、难度分布必须落在目标区间、同一知识点下的题目不能重复出现、同一道题不能被同一考生在不同场次重复抽到。这些约束里任何一条处理不好生成的卷子就是废卷。我踩过最典型的一个坑是总分凑不齐。组卷算法按难度分层抽题抽完发现总分是 97 分目标 100 分于是触发补题逻辑补了一道 3 分的题。看起来没问题但补进来的这道题属于另一个知识点把原本刚好达标的知识点覆盖度给破坏了。这种缺陷单看抽题结果是对的总分对、题型对只有把知识点分布单独拉出来统计才会发现异常。所以我后来在用例里强制加了一条校验组卷完成后必须输出一份分布报告包含总分、题型数量、知识点分布、难度分布、重复率五个指标任何一个指标越界都算失败。另一个高频问题是抽题去重的作用域搞错。去重的作用域至少有三层同一张试卷内不能重复、同一考生跨场次不能重复、同一场次内不同考生之间可以重复但要控制重复率。这三层的实现方式完全不同第一层在内存里做集合判断就行第二层必须落库记录考生历史抽题第三层需要做频次统计。我见过有项目把三层混成一层做结果考生第二次参加补考时抽到了和上次一模一样的卷子。这种问题在测试环境里几乎不会自己冒出来因为你不会主动去造“同一考生考两次”的数据必须专门写用例去撞。组卷模块的用例设计我一般用约束矩阵的方式把所有约束两两配对看是否存在互斥。比如“高难度题占比不低于 30%”和“简答题不超过 5 道”这两个约束如果高难度题里简答题占比很高就可能出现无解的情况——算法必须在限定时间内要么放宽某个约束要么报错。这时候要验证的不是算法能不能解而是解不出来的时候系统怎么表现是静默生成一张不合规的卷子还是明确提示“当前题库无法满足组卷规则”。前者是灾难后者才是可接受的。注意组卷失败的处理路径一定要单独设计用例。很多团队只测“组卷成功”的路径结果真实排考时题库某个知识点被抽空了系统直接返回一张残缺试卷考务人员毫无察觉。2.2 考试过程与异常边界真实考生不会按剧本操作考试过程的测试核心不是“正常作答能不能交卷”而是“各种不正常的情况下考生的答案还在不在”。我做过统计考问通系统上线后收到的考生投诉里超过六成和“答案丢失”或“答案回退”有关而真正因为判分错误引起的投诉不到一成。这个比例很说明问题。异常场景我按触发源分成三类。第一类是网络类断网、弱网、网络切换从移动数据切到无线网络、请求超时、响应乱序。第二类是客户端类页面刷新、浏览器返回键、切到后台再切回、多标签页同时打开同一场考试、多设备同时登录同一账号。第三类是服务端类暂存接口超时、判分任务堆积、考试时间配置被中途修改。这三类里最需要重点投入的是请求乱序。作答页面的暂存通常是定时提交加事件触发提交两种机制并存时就容易乱序。比如考生答完第 20 题触发了事件提交同时定时器正好在提交第 18 题的状态如果服务端没有做版本号或时间戳校验后到的旧请求就会覆盖新状态考生看到的第 20 题答案就没了。验证这个场景的方法很直接在浏览器开发者工具里人为延迟某个暂存请求让它晚于后续请求到达观察最终落库的状态。这个手法我每次做考试类系统都要用屡试不爽。多设备登录这块有个业务判断需要提前和产品对齐同一账号在两台设备上同时进入考试是踢掉前一台还是两台都允许作答、以最后提交的为准两种策略都合理但实现和测试方法完全不同。前一种要验证踢下线的时机和提示文案后一种要验证冲突合并逻辑。我遇到过最糟糕的情况是两套策略都没实现两台设备各自暂存最后交卷时谁先提交谁生效另一台的答案直接丢掉考生完全无感知。这类问题的根因往往是需求没写清楚测试也没往这个方向想。超时自动交卷的边界也值得单独说。系统一般在考试结束时间点触发批量交卷如果 3000 名考生在同一秒被强制交卷瞬时请求量会非常可观。测试时要验证的不只是“能不能交上”还包括自动交卷期间考生手动点击交卷会怎样、自动交卷失败会不会重试、重试失败后考生的答案有没有兜底留存。我的做法是在预发环境把考试结束时间设成几十秒后用脚本模拟三百个在线会话同时跨过结束点观察交卷成功率和失败重试记录。2.3 判分与成绩核算小数点和边界值最伤人判分模块的测试有个特点正常数据怎么测都对出问题的永远是边界。客观题判分逻辑简单但多选题的“少选得分、多选不得分”这种规则如果分值配置支持小数就很容易在累加时出现精度问题。我用 0.5 分、0.25 分、0.1 分这种分值做过一轮专项发现某个实现里用浮点数累加二十道题之后总分出现了 0.30000000000000004 这样的结果直接显示在成绩页面上。修复方式是把分值统一转成整数分乘以 100再计算最后除回去。主观题判分的问题更复杂一些因为它涉及人工阅卷和双评机制。双评的核心是两位阅卷人独立打分分差在阈值内取平均超出阈值转三评。这里的边界值特别多分差恰好等于阈值算不算超阈值、三评分数和双评平均分怎么合成、阅卷人中途退出留下的半成品任务怎么处理。我一般会画一张判定表把所有分支的输入输出列全然后逐行写用例这样不容易漏。成绩核算还有一个隐蔽的点是及格线与总分的一致性。有的考试配置里及格线是百分比比如 60%有的配置是绝对分值比如 60 分如果两种配置方式同时存在很容易出现配置了 60 分但系统按 60% 算的情况。测试方法很土但有效把总分设成 150 分及格线设成 90 分然后构造一个 89 分和一个 90 分的成绩看判定结果。如果系统把 89 分判成及格说明它按 60% 算了89/150 约等于 59.3%应该不及格这时候就能定位到配置读取的问题。成绩复核环节常被忽略但对教学场景很重要。复核意味着成绩发布后还能被修改修改后要留下完整的变更记录谁改的、什么时间改的、原值多少、新值多少、改的原因。测试时我会专门验证两点一是复核修改后统计报表里的数据是否同步更新很多系统报表是定时同步的会出现成绩页新、报表页旧二是复核修改是否触发重新排名如果排名算法是增量更新的很可能漏掉。2.4 性能与容量别用错误的模型压出好看的数字性能测试最容易犯的错是压测模型和真实业务不符。用固定并发一直压接口压出来的 TPS 很好看但真实考试根本不是这种流量形态。真实考试的流量是一条明显的曲线开考后前五分钟是登录和取卷的高峰中间七十分钟是低频的暂存请求结束前五分钟是交卷高峰。三个高峰的请求类型完全不同压力特征也完全不同。我的做法是按阶段分别建模。取卷高峰关注的是并发读和缓存命中率暂存阶段关注的是长连接的维持和低频写的响应时间交卷高峰关注的是瞬时写入和判分任务的排队。这三个阶段分别压分别出结论最后再合成一份整体容量评估。下面这张表是我在某次容量评估里用的分阶段压测参数可以作为模板参考。阶段时长占比并发用户数请求类型关键指标登录取卷前 8%峰值为平均值的 4 倍读为主取卷响应时间 P95正常作答中间 84%平均并发低频写暂存成功率、响应时间集中交卷后 8%峰值为平均值的 5 倍写为主交卷成功率、判分入队时延并发用户数的推算我一般用这个思路单场 3000 人考试时长 90 分钟假设考生在考试过程中的操作是分散的那么平均并发大约在几百的量级但登录和交卷这两个动作是高度集中的按经验大概有七成考生会在开考后十分钟内完成登录取卷这七成里的峰值瞬间并发就需要单独估。具体的数字没法拍脑袋我通常先按经验值设一个初值然后用阶梯加压找出拐点再拿拐点反推能支撑的考生规模。提示压测结论里一定要标注测试数据的题型分布。全是单选题的试卷和含 15% 主观题的试卷判分阶段的压力差好几倍混在一起说没有参考价值。3. 实操过程与核心环节实现3.1 环境搭建与题库造数脚本环境搭建这一步我的习惯是先写一份可重复执行的初始化脚本而不是手工点。考问通系统涉及的表不少题库、试卷、考试场次、考生、作答记录、成绩手工造数据既慢又容易不一致。下面这段是我常用的题库造数脚本骨架用 Python 写核心思路是按题型模板批量生成并保证分布合理。import random import json from datetime import datetime QUESTION_TYPES [single, multiple, judge, blank, essay] TYPE_WEIGHT [0.45, 0.20, 0.15, 0.12, 0.08] KNOWLEDGE_POINTS [fkp_{i:03d} for i in range(1, 41)] DIFFICULTY_LEVELS [1, 2, 3, 4, 5] DIFFICULTY_WEIGHT [0.15, 0.25, 0.30, 0.20, 0.10] def gen_stem(qtype, kp, idx): # 题干长度按题型区分避免全部同质化 base_len {single: 40, multiple: 55, judge: 25, blank: 35, essay: 120}[qtype] jitter random.randint(-8, 25) return f[{kp}] 第{idx}题 内容 * max(8, (base_len jitter) // 2) def gen_question(idx): qtype random.choices(QUESTION_TYPES, TYPE_WEIGHT)[0] kp random.choice(KNOWLEDGE_POINTS) difficulty random.choices(DIFFICULTY_LEVELS, DIFFICULTY_WEIGHT)[0] item { id: fQ{idx:06d}, type: qtype, knowledge_point: kp, difficulty: difficulty, stem: gen_stem(qtype, kp, idx), created_at: datetime.now().isoformat(timespecseconds), } if qtype in (single, multiple): opt_count random.choice([3, 4, 4, 4, 5]) item[options] [f选项{c} for c in ABCDE[:opt_count]] if qtype single: item[answer] random.choice(item[options]) item[score] random.choice([1, 2, 2, 2, 3]) else: correct random.sample(item[options], random.randint(2, 3)) item[answer] correct item[score] random.choice([2, 3, 3, 4]) item[partial_score_rule] random.choice([none, half, by_count]) elif qtype judge: item[answer] random.choice([True, False]) item[score] 1 elif qtype blank: item[answer] [f答案{i} for i in range(random.randint(1, 3))] item[score] random.choice([2, 3]) else: item[answer] 参考答案要点 * random.randint(3, 6) item[score] random.choice([8, 10, 12, 15]) return item def build_bank(total20000): bank [gen_question(i) for i in range(1, total 1)] with open(question_bank.json, w, encodingutf-8) as f: json.dump(bank, f, ensure_asciiFalse, indent1) return bank if __name__ __main__: data build_bank() print(fgenerated{len(data)}) dist {} for q in data: dist[q[type]] dist.get(q[type], 0) 1 print(type_dist, dist)这段脚本有几个设计意图值得说明。题型权重和难度权重不是平均分配而是按真实卷面结构反推的平均分配造出来的题库会让组卷算法的分支覆盖率虚高看起来很全面实际碰不到真实的配比约束。题干长度加抖动是为了避免全文等长导致分页、折叠、富文本渲染这些前端逻辑测不出问题。partial_score_rule这个字段是我特意加上去的因为多选题的给分规则在不同考试里差异很大如果造数时不带这个维度判分模块的那几个分支就永远是死代码。脚本跑完之后我会再跑一段校验检查生成的题库在知识点覆盖度和难度分布上是否接近目标。如果发现某些知识点一道题都没有就补几条定向数据。这一步不能省因为组卷算法里“知识点覆盖度”的约束一旦在题库层面就无法满足后面所有组卷测试都会变成无效测试。3.2 接口自动化用例的组织方式自动化这块我不追求覆盖率数字更看重用例的可维护性。考问通系统的接口数量中等但如果把所有接口都写成独立的断言脚本维护成本会高到没人愿意碰。我的组织方式是三层底层是请求封装和登录态管理中间层是按业务动作聚合的关键字比如“登录并取卷”“作答并暂存”“交卷并轮询判分结果”最上层才是用例。用例本身我用 YAML 描述这样才能让不写代码的同学也参与维护。下面是一个链路用例的描述片段描述的是“迟到考生入场并完成考试”这条路径。case_id: exam_late_join_001 title: 迟到考生在允许迟到时间内入场并正常交卷 priority: P0 tags: [链路, 考试过程, 时间边界] precondition: exam: start_offset_min: 0 duration_min: 90 allow_late_min: 15 submit_mode: auto_and_manual steps: - name: 登录并进入考试 action: login_and_enter params: user: candidate_0001 exam_id: ${exam_id} expect: enter_result: success paper_question_count: 80 - name: 在第10分钟入场 action: wait_until params: exam_elapsed_min: 10 - name: 连续作答20题并暂存 action: answer_and_save params: count: 20 interval_sec: 3 expect: saved_count: 20 - name: 模拟断网30秒后恢复 action: network_interrupt params: duration_sec: 30 - name: 校验断网期间答案未丢失 action: query_answer_state expect: answered_count: 20 - name: 手动交卷 action: submit_paper expect: submit_result: success status: submitted - name: 轮询判分结果 action: poll_score params: timeout_sec: 120 expect: score_not_null: true用 YAML 描述的好处是把“做什么”和“怎么做”分开了。执行层解析 YAML 调用中间层关键字用例变更时只改 YAML不动代码。这套结构在项目里跑了半年多维护成本明显低于纯代码用例。缺点是复杂断言写起来啰嗦我的折中方案是允许在 YAML 里内嵌一小段 Python 表达式只在少数需要复杂计算的断言里用其余场景一律走标准断言。自动化的执行节奏我也调整过。最早是每天全量跑一次跑完出一份报告但全量用例跑一轮要四十多分钟反馈太慢。后来改成三层提交触发的冒烟集五分钟内出结果只覆盖核心链路、每小时跑一次的回归集、每天夜间的全量集。冒烟集失败就阻断合并回归集失败只告警。这个分层不是拍脑袋定的是按“缺陷发现后修复成本”倒推的——越早发现越省钱所以冒烟集必须快。3.3 压测脚本与并发参数的实际推算压测我用过两套工具脚本化能力强的时候用代码写需要快速出图的时候用现成的压测平台。下面这段是用代码写的压测脚本核心部分重点是模拟真实的作答行为而不是无脑打接口。import random import time from locust import HttpUser, task, between class CandidateUser(HttpUser): wait_time between(2, 6) def on_start(self): self.token None self.exam_id None self.answered 0 self.login_and_enter() def login_and_enter(self): resp self.client.post(/api/auth/login, json{ account: fcandidate_{random.randint(1, 3000):04d}, password: test_only }) if resp.status_code ! 200: return self.token resp.json().get(token) headers {Authorization: fBearer {self.token}} enter self.client.post(/api/exam/enter, json{examId: E2024001}, headersheaders) if enter.status_code 200: self.exam_id E2024001 task(20) def answer_and_save(self): if not self.token: return headers {Authorization: fBearer {self.token}} payload { examId: self.exam_id, questionId: fQ{self.answered 1:06d}, answer: [A], clientVersion: self.answered 1, } self.client.post(/api/exam/save, jsonpayload, headersheaders, name/api/exam/save) self.answered 1 task(2) def heartbeat(self): if not self.token: return headers {Authorization: fBearer {self.token}} self.client.get(/api/exam/heartbeat, headersheaders, name/api/exam/heartbeat) task(1) def submit(self): if not self.token: return headers {Authorization: fBearer {self.token}} self.client.post(/api/exam/submit, json{examId: self.exam_id}, headersheaders, name/api/exam/submit)task后面的数字是权重不是次数。暂存请求权重 20、心跳 2、交卷 1这个比例是根据真实考试的行为分布估的一名考生在 90 分钟里大约提交八十次暂存、九十次心跳、一次交卷。权重设错了压出来的瓶颈就是假的。我见过有人把交卷权重设成和暂存一样高结果压测报告显示交卷接口是最大瓶颈实际上真实场景里交卷请求占比不到百分之一。并发规模的推算我一般这么走先用小并发跑一轮拿到单用户的平均响应时间和资源占用然后按目标考生规模做线性外推得到一个理论初值再用阶梯加压验证。举个例子单场 3000 人假设七成考生在开考后十分钟内完成取卷那么取卷峰值的瞬时并发大概在两千一百人的量级但这些人不是同一秒到达的分散在十分钟里按经验取一个集中系数实际压测的时候从 300 并发开始每轮加 100观察 P95 响应时间和错误率找到错误率开始抬头的拐点。拐点找到之后不要直接用拐点数值当结论要留出余量。我的习惯是取拐点的六到七成作为容量上限因为压测环境永远比生产环境干净中间层的网络、缓存、连接池在真实环境里都会打折扣。这个余量不是拍脑袋是我在几个项目里对比压测结果和上线后监控数据得出的经验值。3.4 测试分析报告到底该写什么回到这份报告的本身。标题叫“考问通系统测试分析报告”但很多团队交上来的东西其实是测试执行记录的堆砌跑了多少用例、通过多少、发现多少缺陷、严重级别分布。这些数据有用但撑不起“分析”两个字。分析报告的核心是回答三个问题系统在什么条件下是可靠的、在什么条件下会出问题、出问题的影响范围有多大。我的报告骨架一般是这样第一部分写测试范围与不在范围内的内容明确边界避免后面被追问“为什么没测这个”第二部分写测试环境与数据基线把前面那张基线表放进去第三部分写各维度的测试结论每个维度给结论、给依据、给风险第四部分写缺陷分析不罗列所有缺陷只挑有代表性的和趋势性的第五部分写风险清单和改进建议每条风险要标影响面和触发条件。第三部分是重头戏也是最容易写虚的地方。我的写法是每个维度都用“结论 数据 边界”三段式。比如性能维度结论是“在基线数据下支持单场三千人规模”数据是“峰值并发下的 P95 响应时间和错误率”边界是“当主观题占比超过 25% 时判分时延会超出预期”。三段缺一不可只有结论没有边界的报告读者没法判断这个结论能用到什么场景。缺陷分析我一般不按严重级别简单堆砌而是按根因归类。考问通系统的缺陷根因我大致归成五类状态同步类、时间边界类、并发竞争类、数据精度类、配置读取类。按根因归类的好处是能看出系统性的薄弱点比如状态同步类缺陷如果占了四成说明前后端的状态管理设计本身有问题不是修几个 bug 能解决的得从架构上重新对齐。风险清单我坚持写“触发条件”这一列。没有触发条件的风险等于没说谁都不知道什么时候会踩上。比如“判分任务堆积”这条风险触发条件写成“单场主观题超过八百份且阅卷人同时在线数低于十人”这样运维和产品都能判断自己场景下会不会碰到。4. 常见问题与排查技巧实录4.1 高频缺陷速查表下面这张表是我在这个项目里实际遇到过、并且认为复现价值比较高的缺陷类型整理成速查表遇到类似现象可以先照着对。现象高概率根因快速验证方法处理方向考生刷新页面后答题记录回退暂存请求乱序旧状态覆盖新状态在开发者工具里延迟某次暂存请求服务端加版本号或时间戳校验组卷总分差几分分层抽题后补题破坏其他约束导出组卷分布报告核对五项指标补题逻辑需重新校验全部约束多选题少选得分不对给分规则按选项数算而非按权重构造少选1项、少选2项的用例统一用整数分值计算后除回自动交卷后成绩查不到判分任务入队失败无重试查看判分任务队列的失败记录加失败重试与死信告警同一考生两次考试抽到相同卷抽题去重作用域未覆盖跨场次同一账号连续参加两场去重记录需按考生维度落库成绩复核后报表数据未变报表为定时快照未联动更新复核后立即查报表与成绩页对比复核触发报表重算或标脏迟到考生入场后剩余时间不对剩余时间按入场时刻算而非开考时刻构造迟到 10 分钟入场场景统一以考试结束时间为基准及格判定与配置不符百分比与绝对值配置混用总分设 150、及格线设 90 验证配置项明确单位并加校验这张表我自己用了很久基本上新同学进项目第一周我就会给他让他照着表里的验证方法自己动手复现一遍。复现的过程比看文档学得快而且能顺便验证环境是不是好的。4.2 三个典型故障的排查路径第一个是答案回退。排查这类问题我有一套固定顺序先看前端本地存储里有没有完整答案有的话说明是提交环节丢了没有的话说明是本地存储被清了。然后看服务端的作答记录表按时间倒序取最近十条看有没有版本号倒退的情况。再看网关或接入层的日志确认请求到达顺序和前端发出顺序是否一致。这三步走完基本能定位到是前端提交策略的问题、服务端幂等处理的问题还是中间层的乱序问题。我遇到过一次是前端定时器和事件触发同时提交两次请求几乎同时发出服务端先处理了后发的那个导致版本号倒退。改法是前端只保留一个提交入口用队列串行化。第二个是判分超时。现象是考试结束后超过两分钟还没出分。排查顺序是先看判分任务队列的积压量再看单条判分任务的耗时分布最后看判分服务依赖的资源。有一次发现积压量不大但耗时很长追下去是主观题的相似度计算在逐条跑没有批量化单条耗时随题库规模线性增长。这类问题的处理方向是批量化加缓存而不是加机器加机器只能缓解不能解决。第三个是取卷接口偶发超时。偶发问题最烦人因为不好复现。我的做法是先看超时请求的分布特征是不是集中在某个时间段、是不是集中在某类考生、是不是和某个特定试卷相关。有一次发现超时全都集中在开考后第三到第五分钟而这三分钟正好是缓存预热没完成的时间窗口。根因是缓存过期时间设得太短开考瞬间大量请求穿透到数据库。改法是延长缓存有效期并在考试开始前主动预热。这个问题的教训是缓存策略必须结合业务的时间特征来设计不能照搬通用配置。4.3 踩过的坑和几条实操心得说几条文档里不会写的东西。第一条是关于测试数据的时效性。考问通系统里的考试是有时间属性的你今天造的考试场次明天就过期了所有涉及考试的用例全部失效。我早期的做法是每次执行前重新造数据但造数本身要花十几分钟很拖节奏。后来的解法是把考试时间设成一个相对偏移量脚本执行时动态生成比如“当前时间加十分钟开始、时长九十分钟”这样数据永远有效。这个改动看着小实际把整条自动化流水线的可用性提了一大截。第二条是关于不要相信界面上看到的时间。考试剩余时间这类倒计时前端一般是本地计算加服务端校准两边的时钟一旦有偏差界面显示的时间和实际的服务端判定时间就会不一致。我专门做过一次验证把测试机的系统时间调快五分钟结果界面上显示还剩五分钟的时候服务端已经判超时了。所以所有和时间相关的断言必须以服务端返回的时间戳为准前端显示只能作为辅助参考。这个坑我在两个项目里都踩过。第三条是关于缺陷的复现环境。有些缺陷只在预发环境出现在测试环境怎么都复现不了原因往往是配置差异或数据量差异。我的习惯是遇到这种问题先做环境比对把两边的关键配置项和表数据量拉出来逐项对比十有八九能看出端倪。最没效率的做法是先在测试环境反复试试不出来再去预发试来回折腾半天。第四条心得是关于评审。测试分析报告写完之后我一定会找一个非测试岗位的同事读一遍最好是产品或者运维。如果他们看完之后问“所以这场考试到底能不能撑住”说明报告没写到位结论不够直白。好的报告应该让不看细节的人也能在三十秒内拿到核心结论细节留给需要的人往下翻。我现在的做法是在报告开头放一段不超过两百字的结论摘要把“能支撑多大规模”“有哪些已知风险”“哪些场景没覆盖”三句话说清楚。最后分享一个我在报告里固定加的小节叫“本轮测试未覆盖的场景”。这一节经常被要求删掉理由是显得测试不充分但我坚持保留。原因很实在任何测试都有边界把边界写清楚后续出问题时大家知道去哪找原因而不是一上来就怀疑测试漏了。写清楚没测什么比含糊地说“已充分测试”要负责得多。这个习惯是我在做第一个大型项目时被一次线上故障教出来的那次故障恰好落在一个没人明确说过“不测”的角落里。