接手一个OJ在线评测系统的测试任务看到标题里的“测试报告”三个字很多人第一反应是找模板。但真在测试岗位待过几年的人都知道模板是最不值钱的东西值钱的是你有没有把OJ这种特殊系统当成“普通Web系统”去测。我翻了一圈市面上的资料发现搜“OJ在线评测系统 测试报告”的人多半卡在几个点上不知道从哪儿下手设计用例不知道JMeter压测出来的数据怎么解读也不知道报告里除了功能用例还要写什么。这篇直接把我在OJ平台上的完整测试思路、脚本、数据组织方式和报告结构摊开讲照着做至少能少走三个月弯路。1. 别把OJ当成普通Web项目来测被测对象与测试分层1.1 OJ系统的核心链路到底长什么样大部分OJ在线评测系统的架构套路差不多前端页面提交代码请求经过网关或业务API先把提交记录落库然后塞进消息队列判题机作为消费者从队列里取任务在隔离环境里编译运行用户代码再把判定结果写回去前端通过轮询或WebSocket拿到结果。这条链路里最有“OJ特色”的是后半段也就是从消息队列到判题机再到结果返回的过程。普通Web测试关注的是接口返回正不正确、页面交互顺不顺畅而OJ测试必须要回答一个更底层的问题用户写的那段代码到底有没有被公平、准确、安全地判定。所以接手项目的第一件事不是打开JMeter而是先把架构图画出来把一条提交从发出到拿到结果的完整时序走一遍。我当时还专门找开发要了判题模块的wiki确认了三个关键点判题机是单机还是多机用的是容器隔离还是进程级沙箱题目数据是存在对象存储还是挂在本地磁盘。这三个问题直接决定了后面测试方案怎么写。1.2 我习惯把测试拆成四个层次接到OJ这种系统我不会把测试用例一股脑堆在Excel里。我惯用的拆法是四层第一层前端与业务接口层。登录、注册、题目列表、查看题目详情、提交代码、查看提交记录、排行榜、竞赛管理。这块和普通Web测试没什么两样可以复用大部分常规接口测试经验。第二层判题核心层。代码提交之后评测系统能不能准确区分各种结果状态不同语言、不同编译选项、不同资源限制下是否都能稳定判定。这一层是OJ测试的重点也是最容易出漏网之鱼的位置。第三层题目数据与配置层。题目测试点数据本身合不合理输入输出格式定义是否和题目描述一致是否有标准特判程序SPJ数据范围是否覆盖了边界值。很多测出“误判”的bug最后定位出来其实是题目数据写错了。第四层系统级与容错层。并发提交、判题机宕机、消息队列积压、磁盘写满、沙箱被恶意代码冲击系统能不能自愈会不会丢提交。这一层通常会在项目快要上线时被压缩得最狠反而是OOM和卡死的高发区。1.3 和普通业务系统测试最大的差异说到底最大的差异在于OJ系统里存在一份不受测试人员控制的“用户代码”。普通系统测试时后端行为基本可预期OJ系统里判题机面对的是不可信的、可能恶意也可能只是写错的用户代码测试必须覆盖“代码行为不可控”带来的异常。举个最简单的例子普通Web系统做性能测试无非设置一些典型参数去请求接口但OJ压测一定要带上真实的“判题负载”也就是让判题机真的去编译运行一批用户代码。空跑接口只能打满API层根本摸不到判题机的CPU和内存上限。我第一次测试时就是因为只压提交接口导致上线后考试场景一脚踩进泥坑判题积压到三分钟才出结果。后面第三部分会专门展开。2. 判题核心链路从提交到结果返回的验证要点2.1 结果语义验证不是“能跑就行”是要严谨到空格判题机把用户代码的运行结果翻译成状态码常见的有AC正确、WA答案错误、TLE超时、MLE内存超限、RE运行时错误、CE编译错误、SE系统错误。初学者测到这里就觉得够了但真正容易被放过去的坑是结果状态与代码行为的对应关系。我自己准备了一套“最小验证样例集”每种状态用一段尽可能简单的代码触发然后核对判题返回。拿WA来举例不能只验证“输出不一样就返回WA”还要测“输出内容完全一样但末尾多一个换行”算不算WA“每个数字后面多一个空格”算不算WA。不同OJ的判题策略配置不一样有的严格按字节比对有的在比对时会忽略行尾空白。这套逻辑不测到位就会出现那种“我在本地跑对了OJ就是不认”的经典学生投诉往往一个问题就能炸掉整个答疑群。为了验证这种比对策略建议针对同一道整数输出题准备三类程序精确匹配、末尾多空行、行内多空格。提交三次看结果比对方式和题目配置对不对一眼就能看穿。2.2 资源限制的合规性验证时间、内存、输出体积判题系统通常会给每种语言配置资源配额比如C时限1秒、内存256MBJava时限2秒、内存512MB。测试要做的不是“开发说配了我就信”而是实际去构造消耗资源的代码验证限制真的生效。时间限制这块我会提交两个程序一个空转死循环while (1);触发TLE一个在极限边缘运行的斐波那契递归看它接近时限时的表现。这里要特别关注判题系统对CPU时间和真实经过时间的口径。最理想的方案是只计算用户进程的CPU时间而不是墙钟时间否则服务器本身负载一高正常代码也可能误杀成TLE。内存限制更考验细节。我踩过的一个真实坑是某语言运行时的虚拟机需要预留额外内存但OJ系统在统计时只算堆内存没算JIT编译和GC的额外占用导致256MB限制下出现过几次MLE误判。测试时建议用指数增长的程序逐步逼近内存配额看看判题系统是在接近限制还是超过限制才终止。输出体积也经常被忽略。有些判题沙箱默认限制输出文件大小比如32MB超了会直接返回RE或输出超限而不是TLE。不能把输出超限和普通RE混在一起报告里如果看到大批“总在超限点挂掉”的提交就要怀疑输出文件没有按单测试点重置。2.3 判题机与评测队列的异常恢复这条经验值很多钱OJ系统上线后最影响口碑的不是单次判题错误而是提交丢了、判题机挂了之后队列卡死。所以异常恢复测试不能只在功能测试阶段做。我会用两种方式验证提交一批代码到队列里然后手动停掉一个判题Worker再启动它确认积压任务被重新消费而且不会被重复消费出双倍结果。把数据库里某条提交的状态人为改成“判题中”并重启服务观察系统是否会把这种悬挂状态自动重置为“等待判题”或标记为失败而不是让用户永远卡在“判题中”。这个场景用JMeter跑不出来必须写脚本介入或者让开发配合做故障注入。测试报告里我会把这类验证单列成“容错性专项”因为普通压测报告根本不会覆盖到。3. 赛事场景下的并发与峰值用JMeter把系统打到真实水位3.1 为什么JMeter能出测试报告而且够用“JMeter 能出测试报告吗”这个问题答案非常明确能而且HTML报告挺完整不需要再买商业工具。JMeter压测完能自动生成包含聚合报告、响应时间分布、吞吐量、错误率的静态HTML页面用来评估OJ系统整体并发能力完全够。但必须说清楚JMeter的边界它压的是HTTP链路真正把代码丢给沙箱编译运行这件事JMeter本身不参与。做到“更真实”要在脚本设计里让一部分线程真的去提交代码并不断轮询判题结果让判题机承担真实的编译和运行负载。3.2 测试场景与脚本设计我一般建三个线程组对应三种典型场景。场景一日常练习。模拟几十个用户同时浏览题目、提交代码重点看API层和数据库读写整体压力不大主要验证稳定性和日志有没有异常刷屏。场景二竞赛瞬时峰值。在线比赛最常见的画面是开赛前大家都在刷新、比赛一开始前几分钟大量提交同时涌入。在线考试系统的特征更明显某三分钟交卷窗期内几百人同时点提交。我建模的时候会设置一个“同步定时器”让线程在指定时间点同时起步而不是均匀分散地点击。场景三全链路持续压测。所有线程都做同一件事提交一份固定解法代码然后轮询判题结果直到返回AC。这样直接把判题Worker的CPU打满观察整条链路的吞吐和积压。JMeter脚本参数化也很关键。把并发数、持续时间、提交代码的文件路径用JMeter属性外置这样命令行跑不同轮次不用改脚本命令大致是这样jmeter -n -t oj_full_link.jmx -l result.jtl -j jmeter_run.log -e -o ./html_report -Jthreads100 -Jduration900跑完后打开html_report目录下的index.html响应时间分位数、TPS曲线、错误率都在里面。我会再手动拉一份聚合数据CSV存到项目的测试资产目录方便后面写报告时引用具体数字。3.3 监控指标与瓶颈定位光看JMeter的结果页会误判问题。判题场景下JMeter测到的“提交接口响应快”不代表判题快因为提交动作只是把任务丢进队列。真正需要监控的是这几个指标队列积压数这是核心。积压曲线持续上涨说明判题Worker的处理速度跟不上提交速度。判题Worker的CPU使用率CPU打满未必是坏事说明它在满负荷干活需要结合积压数判断是“健康打满”还是“处理不过来”。编译进程数和沙箱容器数如果容器启动速度成为瓶颈积压也会上涨这时候加大Worker数量没用要先解决容器启动的并发上限。数据库连接池与Redis连接数提交记录落库和判题结果回写容易出现连接池占满现象是接口偶发超时但判题机本身CPU不高。我会开着dstat、vmstat和top -H盯这些指标并用Grafana把队列消费延迟拉出来。没有监控体系的话事后定位问题会非常痛苦所以我是建议在压测前就把基础监控和日志采集配好。3.4 一批实测数据长什么样示例下面这些数字我抹掉了具体项目名只展示报告里常用的表格形态。表中数据仅作为“结果如何呈现”的参考样例不代表任何线上系统的承诺值。场景并发用户数提交接口平均响应时间判题完成P95队列最大积压Worker平均CPU日常练习混合50180ms420ms1535%开赛峰值提交120620ms1.8s28090%全链路持续压测100450ms2.3s16085%实际写报告时表格下面一定要配一句解读不能只把表扔上去。比如“全链路持续压测时队列积压虽有波动但能在40秒内消化完毕没有出现不可恢复的堆积判题P95在第25分钟后小幅上升怀疑与GC活动有关建议持续观察”。4. 边界输入与判题安全最容易在测试阶段翻车的地方4.1 恶意提交清单怎么设计OJ系统天然会被用户拿去做各种“攻击实验”有的出于好奇有的真的是恶意。作为测试人员必须替系统预先趟一遍雷。我这里会构造一份判定安全的边界用例清单不会真让它在生产环境裸奔而是在专用测试环境执行死循环或永不退出的递归验证CPU时间限制能不能在预期时间内强制终止。大块内存申请验证内存额度限制以及超限时是否能干净地回收进程而非拖垮宿主机。无限向标准输出打印内容验证输出文件大小限制和磁盘保护。大量创建子进程或线程验证进程数的限制与隔离策略。尝试读写无关文件、连接外部网络的代码验证沙箱是否真正限制了系统调用和网络权限。每一类用例我关注的都不是“用户能否得逞”而是沙箱失败后系统是否仍然稳定、记录是否完整、旁路的进程有没有被彻底回收。如果某个恶意提交导致判题Worker退出或宿主机负载异常这是P0级别的安全缺陷。4.2 环境隔离与资源回收隔离性测试要看到的现象是一个用户代码把内存吃满判题机不能跟着崩一个用户代码把CPU占满另一个判题任务不能陪跑被拖死。多数OJ会用容器做隔离但容器本身并不可怕可怕的是没有配好资源限制的容器比如没有cgroup约束也没设置最大文件大小。在测试报告里我会明确写一项“隔离性验证结果”包括相同提交连续执行五次前一次的进程残留是否会影响后一次判题容器是否无法访问宿主机上其他用户的提交代码目录超出资源配额的用户进程被杀死后临时目录是否会被清理干净。哪怕开发已经做过一遍测试侧也要独立验证一遍。4.3 安全策略验证里的三个真实教训第一个教训是关于内存统计口径的。某次测试里一个Java提交运行完所有测试点后返回MLE开发查了半天发现是系统把沙箱整体的内存峰值都算进去了包含了解析器加载带来的额外开销。最后按语言维度调整倍数才解决。所以测试报告里一定要记录“不同语言下的内存倍率验证记录”不能默认所有语言一个标准。第二个教训是环境变量残留。判题沙箱复用时上一份提交设置的环境变量没清理干净导致下一份用户的程序读到了不该读的配置。测试时要特意提交一个打印环境变量的程序对比多次运行结果是否完全一致。第三个教训是关于临时文件的。有一次压测过程中发现磁盘空间迅速变小最后定位是某个误判成RE的提交在沙箱里生成了超大临时文件而清理任务只覆盖了正常结束的路径。从那以后我每次做异常恢复测试都会顺手检查临时目录的清理情况。5. 测试报告怎么组织才不会被开发、产品和评委追问到崩溃5.1 报告骨架按阅读对象分层一份好报告要先让决策者用一分钟看懂结论再让开发能按图索骥复现问题最后让评委或客户看到测试覆盖的完整度。我常用的骨架是测试概述版本、环境、时间、测试范围、参与人员把被测代码的commit号写清楚。测试结论与风险评估先说能不能上线有哪些风险必须接受。测试统计用例总数、通过数、失败数、阻塞数、缺陷按模块分布。功能测试结果按模块拆解每个模块有通过率与问题摘要。判题核心链路专项结果结果语义覆盖矩阵资源限制验证数据。性能与稳定性测试结果JMeter场景说明、实测指标表、监控截图、瓶颈分析。安全与边界测试结果恶意提交用例结果、隔离性验证结论。缺陷清单与处理追踪列表给出问题描述、优先级、复现步骤、状态。附录JMeter脚本位置、原始测试样例代码、环境配置说明。5.2 缺陷怎么分级和历史缺陷怎么定量缺陷分级我会按影响面来而不是按心情优先级定义示例P0 致命系统崩溃、核心链路不可用、存在越权或数据丢失风险高并发下同一条提交被重复判题产生两条结果记录P1 严重关键功能不符合预期影响用户判题正确性Python提交在特定条件下误判为MLE或REP2 一般功能可用但有边界缺陷有临时规避方案参赛倒计时页面与服务器时间存在3秒偏差P3 轻微不影响核心链路体验或提示信息问题提交记录页在部分浏览器下样式错位写报告的一个习惯是给每个P0/P1缺陷都附一段“可复现过程”。比如“提交附带的1GB内存申请代码在判题配置为256MB的题号#1024上运行预期返回MLE实际返回RE且判题Worker日志出现异常堆栈”这种描述能让开发直接定位而不是跑来找你反复问。5.3 结论怎么写得既不背锅又不甩锅“测试结论”是最容易被抠字眼的部分。我的写法是给出明确建议同时把残留风险都摊开。例如“当前版本在功能覆盖范围内满足上线要求判题核心链路在200并发以内的表现符合预期P0级缺陷已清零P1级缺陷中问题#JUDGE-10237暂无可靠规避方案建议在赛事类场景上线前完成修复后再放量。”这里的要点是不写“系统很稳定绝对没问题”这种话也不写一堆模棱两可的“可能大概也许”。压力测试结论要写清楚“在多少并发、持续多久、什么模型下”有效这个限定条件比结论本身更值钱。6. 收尾我在多轮OJ测试里踩过的坑如果只留三句话给后来人我会说第一做OJ测试永远先准备一组“答案已知的黄金程序”分别对应AC、WA、TLE、MLE、RE、CE、SE。无论系统怎么改版每个版本先拿这组程序回归一遍判题核心链路能筛掉一大半低级回归问题比把所有功能用例重跑一遍划算得多。第二压测时千万别只打提交接口要把判题Worker的真实负载跑起来。很多OJ系统的性能问题都藏在沙箱创建和编译进程调度里光靠HTTP压力请求触发不到这条路径。第三一定把测试资产留全。JMeter的jmx脚本、压测CSV原始数据、每个状态码的触发样例代码、环境参数说明、服务日志全部打包放到项目文档库里。这份资产在下次版本升级、架构改造、容量规划的时候都能复用。我第一次因为懒得存档三个月后系统重构时连当时压测用的并发模型都找不到只能重新测一遍血亏。我记得很清楚第一次独立负责OJ系统测试时最焦虑的不是不会写用例而是找不到一个“标准答案”告诉我测到什么程度才算完。现在我自己的答案是把判题结果语义验证透、把资源约束验证透、把并发峰值和队列恢复能力验证透、把隔离安全性验证透然后写一份能让人对着复现问题和了解风险敞口的报告。做到这四件事这个系统的测试报告就立住了。