ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

蓝桥杯软件测试模拟赛1期六模块全流程备考指南

蓝桥杯软件测试模拟赛1期六模块全流程备考指南

1. 先摸清楚模拟赛1期到底在考什么

蓝桥杯软件测试这个赛道,跟纯算法赛道完全是两套玩法。算法赛是给你一道题,写代码跑用例,过了就是过了;软件测试赛更像真实项目交付——给你一个被测系统、一份需求说明,然后要求你在有限时间里把测试用例、自动化脚本、接口集合、性能脚本、单元测试代码和一份报告全部产出来。第十五届模拟赛1期的题目,我拿到手第一反应是:这不是在考“你会不会用工具”,而是在考“你能不能在时间压力下,把一个测试流程跑完整”。这两个能力差得远。

先把结论摆出来:模拟赛1期的题目结构,基本沿着功能测试、自动化测试、接口测试、性能测试、单元测试(白盒)+ 测试文档这条主轴铺开,分值大致按 20/25/20/20/15 的比例切分(不同场次会有浮动,这是基于多届真题结构的合理推断,具体以当场试卷为准)。这个结构其实非常“工程化”——它默认你已经会点 Python、会点 Selenium、会看 JMeter 的聚合报告,然后用一个完整的业务场景把这些工具串起来。所以你在准备的时候,重点不该是“再学一个工具”,而是“把一条从需求到缺陷单的链路练到闭着眼睛也能走完”。

1.1 赛题结构的拆解逻辑

我一般把这类赛题拆成三层来看。最上层是业务层:被测系统通常是一个裁剪过的 Web 后台,比如一个商品管理、订单处理或者人员信息维护的模块,功能点不会太多,十几个页面顶天,但每个功能点都会埋一些“看起来正常、实际有坑”的地方。中间层是工具层:Selenium/Playwright 做 UI 自动化,requests 或 Postman 做接口,JMeter 做压测,JUnit 或 pytest 做单元测试。最底层是表达层:用例怎么写、缺陷单怎么描述、报告怎么组织——这一层最容易被忽略,但恰恰是拉开分数的地方。

为什么这么拆?因为评分是分模块独立打分的,工具用错顶多丢一个模块的分,但表达层拉胯会同时污染好几个模块的得分。我见过有人自动化脚本跑得飞快,结果缺陷单里“复现步骤”只写了“点击按钮报错”,评委根本没法复现,分就没了。所以我的建议是:工具熟练度练到“能用”就够,把剩下的精力砸在用例设计和缺陷描述上,性价比高得多。

再说一个容易被忽略的点:模拟赛的题目里,需求描述往往是不完整的。真实项目里需求文档也是不完整的,这是刻意还原。比如需求里写了“用户名长度 6-20 位”,但没写“是否允许中文”“是否区分大小写”“首尾空格怎么处理”。这些空白不是出题人疏忽,而是考点——它考你能不能用等价类和边界值把这些隐含规则挖出来,写成可执行的用例。你在赛场上遇到这种模糊描述,别急着抱怨,先把能确定的写死,把不确定的列成“待确认项”,这个动作本身就是加分项。

1.2 各模块能力要求的映射关系

我整理了一张表,把每个模块考什么、需要什么前置能力、最容易丢分的地方列出来,你可以对着自查:

模块核心考点前置能力高频失分点
功能测试用例设计方法组合、缺陷描述质量需求分析、等价类/边界值/判定表用例覆盖不全、只测正向不测反向
自动化测试元素定位稳定性、等待策略、断言Python 基础、Selenium/Playwright硬编码 sleep、定位器脆弱、只跑不断言
接口测试请求构造、状态码与业务码校验、依赖传递HTTP 协议、JSON 结构、抓包只断言 200、忽略字段级校验、token 未关联
性能测试场景建模、线程数计算、报告解读JMeter 元件、并发概念线程数拍脑袋、断言缺失、报告只贴图不分析
单元测试覆盖率目标、参数化、MockJUnit/pytest、分支逻辑只写语句覆盖、断言太弱
测试文档结构完整性、术语准确测试计划/报告规范章节缺失、结论无数据支撑

看懂这张表你就明白,这个赛项真正的门槛不在工具,而在方法论。等价类、边界值、判定表这些词你可能在课本上背过,但真给你一个“会员折扣计算”的需求,让你 20 分钟内产出 30 条有效用例,很多人就卡住了。我在下面第 2 节会用具体例子把这条路走一遍。

1.3 开赛前必须做完的环境自检

这个环节看着无聊,但每年都有人栽在这上面。模拟赛一般允许自带电脑或者用赛场机器,不管你用哪种,开赛前 15 分钟必须把这几件事做完,做成一个清单贴在脑子里:

  • Python 版本与包:确认python --version是 3.8 以上,pip list里selenium、requests、pytest、pytest-html、openpyxl、PyYAML都在。离线环境要提前把 whl 包下载好放本地目录,pip install --no-index --find-links=./pkgs -r requirements.txt这条命令要提前试通。
  • 浏览器与驱动:Chrome 版本和 chromedriver 版本必须对齐,大版本号一致就行。我踩过的坑是驱动在 PATH 里但版本不匹配,报SessionNotCreatedException,排查半天。稳妥做法是在脚本里显式指定Service(executable_path=...),别依赖环境变量。
  • JMeter:确认bin目录下有jmeter.bat/jmeter.sh,启动后语言切换成中文(Options → Choose Language → Chinese),JVM 内存参数调一下,默认 512m 跑几百线程容易 OOM,改成HEAP=-Xms1g -Xmx2g。
  • 数据库客户端:很多验证点需要直接查库确认数据落库是否正确,DBeaver 或者命令行 mysql 客户端提前连好被测库,把连接信息存下来。
  • 截图工具与目录:建一个固定的evidence/目录,脚本里失败截图自动存进去,报告里直接引用相对路径。

提示:环境自检不要等到开赛才做。模拟赛通常提前开放环境,你可以在赛前用一套“冒烟脚本”跑一遍——打开首页、登录、点三个菜单、截图保存,能跑通说明环境没问题。这套脚本五分钟能写完,能帮你省掉开赛后半小时的慌乱。


2. 功能测试题:用例设计和缺陷单才是基本盘

功能测试模块在所有模块里分值占比不是最高,但它是最“确定性”的一块——只要你方法用对、覆盖到位,分基本能拿满。反过来说,这块丢分最冤枉,因为它不依赖任何工具熟练度,只依赖你对需求的理解深度和表达的规范度。我带过的人里,工具玩得飞起但用例写得稀碎的不少,最后总分被这块拖后腿。

模拟赛1期这类题目的典型形式是:给你一段 800-1500 字的需求描述,可能配一两张原型图,要求你在 40-60 分钟内产出一份测试用例表(通常给 Excel 模板,字段包括用例编号、模块、标题、前置条件、步骤、预期结果、优先级),外加一份缺陷报告。需求里一定有可挖的规则,比如金额精度、状态流转、权限边界、并发冲突。

2.1 需求分析的切入顺序:先画流程再拆点

我的习惯是拿到需求先不急着写用例,花 5 分钟做三件事:画主流程图、标状态节点、圈模糊词。

画流程图是为了找场景法的主干和分支。比如“用户提交订单”这个需求,主干是“选商品→确认订单→支付→订单完成”,分支至少有:库存不足、优惠券不可用、支付超时、重复提交、金额为 0、地址为空。这些分支不是靠灵感想出来的,是流程图上一个节点一个节点问出来的:“这个节点如果失败会怎样?如果中途退出会怎样?如果两个人同时操作会怎样?”

标状态节点是为了判定表。凡是出现“状态”的需求,几乎必然要用判定表。比如订单状态有“待支付、已支付、已发货、已完成、已取消”,每个状态下允许的操作不同(能否取消、能否退款、能否评价),这就是一张二维表,横着是状态,竖着是操作,交叉点写允许/不允许。这张表一画出来,用例数量基本就确定了,不会漏。

圈模糊词是为了找隐含用例。需求里出现“合理”“适当”“及时”“正常”这类词,全是模糊地带,必须拆成可验证的条件。“用户名长度合理”就要追问:下限是多少、上限是多少、边界值取不取、超长怎么提示、空值怎么处理、纯空格算不算合法。“及时刷新”就要问:刷新周期是多少秒、手动刷新按钮有没有、刷新失败怎么提示。

心得:这三步做完,我会把“待确认项”单独列一栏。赛场上没法真的去问产品经理,但你可以自己给出“假设”,并在用例里注明“假设按 X 处理”。评委看到你有这个意识,印象分直接上去了。

2.2 用例设计方法的组合拳怎么打

单用等价类会漏边界,单用边界值会漏组合,单用场景法会漏数据校验,所以真实赛题上一定是组合用。我按优先级排一下:

第一顺位是等价类 + 边界值,因为性价比最高。凡是涉及数值、长度、数量、日期的地方,一律先划等价类再取边界。举个模拟赛常见的例子:某输入框要求“数量为 1-999 的整数”。等价类划分就是:有效等价类 1~999 的整数;无效等价类分四种——小于 1(0、-1)、大于 999(1000)、非整数(1.5)、非数字(abc、空、特殊字符)。边界值取 0、1、2、998、999、1000,这六个点必测。

第二顺位是判定表,用于多条件组合的业务规则。比如“折扣计算”这种题:会员等级(普通/银卡/金卡)× 订单金额(<100、100-499、≥500)× 是否使用优惠券(是/否),三个条件组合起来 18 种情况,每种情况算出一个期望折扣。判定表画出来,每一列就是一条用例,一条不漏。这里有个技巧:先画全表再合并,把动作相同的列合并掉,能省一半用例量,但合并要写在备注里说明“合并依据”,否则评委会觉得你漏测。

第三顺位是场景法,用于端到端业务流程。主流程一条,每个异常分支各一条。异常分支的挖掘口诀是“断、错、退、重”:断(网络中断、会话过期)、错(输入错误、数据异常)、退(中途取消、返回上一步)、重(重复提交、并发操作)。

第四顺位是错误推测法,用来补漏。这个靠经验,比如:输入框粘贴超长文本、输入 emoji、浏览器后退后重新提交、修改 URL 参数越权访问、并发点击两次提交按钮、系统时间跨天时优惠是否失效。这些点在需求里通常不写,但都是真实 Bug 高发区。

我把这四种方法对应到一张速查表里,赛场上直接照着走:

需求特征首选方法补充方法典型用例数
单字段数值/长度校验等价类+边界值错误推测8-12 条
多条件组合规则判定表等价类按列数算,通常 10-20 条
流程/状态流转场景法状态迁移图主干 1 条 + 分支 6-10 条
权限/角色判定表场景法角色数 × 操作数
列表/分页/搜索等价类+边界值错误推测8-10 条

这里我要强调一点:用例数量不是越多越好。模拟赛的评分逻辑通常是“覆盖关键测试点得分 + 冗余用例扣分或不加分”。我见过有人写了 80 条用例,其中 40 条是重复的“输入正确值,提交成功”,把真正的边界和异常淹没了。宁可 30 条条条见血,不要 80 条注水。

2.3 一条能被评委看懂并复现的缺陷单

缺陷报告这块,很多人栽在“写给自己看”而不是“写给看不懂业务的人看”。模拟赛的评委可能是自动化判分,也可能是人工看,两种情况下结构完整、描述可复现都是硬指标。我用的字段是这样的:

字段填写要点反例
缺陷标题模块+操作+现象,一句话说清“有个 Bug”(太笼统)
所属模块精确到子功能“订单模块”(不够细)
严重程度致命/严重/一般/轻微,按影响定全填“严重”
优先级高/中/低,按修复紧迫度定和严重程度混为一谈
前置条件账号角色、初始数据状态留空
复现步骤编号、可执行、每步一个动作大段文字糊在一起
预期结果引用需求原文或明确规则“应该正常”
实际结果客观描述现象,可带截图“不行”(无信息量)
附件截图/日志,命名规范无

严重程度和优先级的区分是高频扣分点。严重程度看影响面(是否阻断主流程、是否丢数据、是否安全风险),优先级看修复顺序(是否影响上线、是否有替代方案)。比如“订单列表页金额显示少了个小数点”是严重程度一般但优先级高;“帮助文档链接 404”是严重程度轻微优先级低。这个区分你写清楚了,评委一看就知道你是懂行的。

复现步骤的写法我有个模板化思路:每步只做一件事,每步的状态可验证。比如:

  1. 使用测试账号登录系统,进入“商品管理”页面
  2. 点击“新增商品”,填写商品名称“测试商品A”,价格填“-10”,点击保存
  3. 观察页面提示与列表数据
  4. 刷新页面,重新进入商品列表,查看该商品是否入库

这四步的好处是:第 3 步验提示,第 4 步验数据落库,两个维度都覆盖了。很多人的步骤只写到“点击保存”,没写验证动作,导致评委不知道你到底看到了什么。

注意:截图一定要截全屏或者截关键区域带上下文,别只截一个小红字。我在实战里被坑过——只截了提示文字,看不出是哪个页面、哪个操作触发的,结果这条缺陷被判定“无法复现”直接作废。

2.4 功能测试模块的常见坑

第一个坑是只测正向。正向用例写起来顺手,但分值低。赛题的测试点设计通常是正向 30%、反向 50%、边界 20%,你把反向全漏了,直接腰斩。

第二个坑是优先级乱填。如果模板里有优先级字段,一定要有区分度,全填“高”等于没填。合理的分布是 P0(阻断主流程)10%、P1(核心功能异常)30%、P2(一般)50%、P3(体验)10%。

第三个坑是用例步骤和预期结果不匹配。步骤里做了三个动作,预期结果只写了一个,评委没法判断你到底验了什么。原则是:每个可观察的状态变化都要有一个对应的预期结果。

第四个坑是忽略数据准备和清理。用例里写“输入已存在的用户名”,但你得说明这个用户名是系统预置的还是你新建的。如果涉及数据修改,还要说明测试后是否需要恢复,否则后续用例会互相污染。


3. 自动化测试题:稳定性比花哨重要

自动化测试模块一般给一个明确的操作流程,比如“登录→进入某页面→新增一条记录→校验列表出现该记录→删除→校验列表消失”。要求你用 Python + Selenium(或者 Playwright)把这段流程脚本化,跑通并生成报告。看起来简单,但这是丢分重灾区,因为它同时考“能不能跑通”和“跑得稳不稳”。

我的核心观点:这个模块的评分标准本质上是“可重复执行”。一次性跑通不算本事,跑十次十次通过才叫本事。所以你在写脚本的时候,脑子里要一直绷着一根弦:什么东西可能导致这次跑通、下次挂掉?

3.1 元素定位策略:稳定优先,简洁其次

定位器选择上,我的优先级是:id > 稳定的 data 属性 > name > 相对 CSS/XPath > 绝对 XPath。绝对 XPath(/html/body/div[3]/div[2]/...)是自杀式写法,页面结构一改就废,而且赛题系统的 DOM 结构经常带动态生成的类名。

如果页面元素没有 id,我一般用“属性 + 文本”组合的相对 XPath,比如:

# 推荐:语义清晰,抗结构变化 ADD_BTN = (By.XPATH, "//button[normalize-space()='新增']") # 备选:结合父级容器限定范围 ROW_BY_NAME = (By.XPATH, "//table//tr[td[normalize-space()='{name}']]")

这里用normalize-space()是为了处理文本前后有空格或换行的情况,很多系统按钮文字里带隐藏空格,直接text()='新增'匹配不上,这是个特别隐蔽的坑。

还有个技巧:定位器集中管理。不要把find_element的字符串散落在各处,全部提到常量或者 Page Object 类里。这样一旦某个定位器失效,你改一处就行,赛场上时间宝贵,别做全局搜索替换。

3.2 等待策略:显式等待是唯一正解

time.sleep()是新手最容易犯的错,也是评委一眼能看出来的低级问题。原因很简单:固定等待要么不够(网络慢就挂),要么浪费(每次都等满 3 秒,20 个步骤就是 1 分钟)。正解是显式等待,等到条件成立立刻继续。

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10, poll_frequency=0.5) # 等元素可点击再点,比等元素出现更可靠 wait.until(EC.element_to_be_clickable((By.ID, "submit"))).click() # 等列表里出现目标行,而不是等页面加载完 wait.until(EC.text_to_be_present_in_element( (By.CSS_SELECTOR, "#record-list"), "测试商品A"))

超时设多少?我一般设 10 秒。设太短(3 秒)在系统响应慢时误报,设太长(30 秒)真出问题时要等很久才暴露。10 秒是个平衡点,配合 0.5 秒轮询,兼顾灵敏度和稳定性。

更稳的做法是封装一个wait_and_click和wait_and_type,把“等待 + 滚动到可见 + 点击 + 异常重试”打包在一起。有些元素被浮层遮挡,直接点会报ElementClickInterceptedException,先scrollIntoView再点能解决大半。

def safe_click(driver, locator, timeout=10): el = WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator)) driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el) el.click()

3.3 断言设计和数据驱动

自动化脚本如果没有断言,那就是“录制回放”,跑完不知道对不对,这是最典型的丢分点。断言要分层次:

  • 页面级断言:操作后是否跳转到正确页面、是否出现成功提示(可以用 URL 包含某段路径,或者提示文字出现)
  • 数据级断言:列表里是否新增了这条记录、字段值是否与输入一致
  • 状态级断言:删除后该记录是否消失、总数是否减 1

不要只做第一层。我见过脚本点完保存就断言“页面没报错”,这等于什么都没验。

数据驱动这块,如果赛题要求批量跑多组数据,用pytest.mark.parametrize是最省事的:

import pytest @pytest.mark.parametrize("name,price,expect", [ ("商品A", "99.99", "保存成功"), ("商品B", "0", "价格必须大于0"), ("商品C", "-1", "价格必须大于0"), ]) def test_add_product(driver, name, price, expect): page = ProductPage(driver) page.add(name, price) assert expect in page.get_toast()

这个写法有两个好处:一是一组数据一个用例,报告里清晰;二是失败时能定位到具体是哪组数据挂的,方便排查。

3.4 自动化模块的三个高频事故

事故一:会话失效导致全局挂掉。很多系统有登录超时,跑到后半段突然被踢回登录页,后面全部报NoSuchElement。解决办法是在 fixture 里做“健康检查”——每次操作前判断当前 URL 或某个标志元素,如果掉登录页就重新登录。

事故二:执行顺序依赖。新增依赖删除、删除依赖新增,如果用例执行顺序被打乱就全乱套。解决方法是每个用例自己造数据、自己清理,或者在 fixture 里做 setup/teardown,别依赖上一个用例的残留状态。

事故三:报告没有失败证据。跑挂了,评委(和你自己)都不知道为什么。加一个失败截图 hook:

@pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() if rep.when == "call" and rep.failed: driver = item.funcargs.get("driver") if driver: driver.save_screenshot(f"evidence/fail_{item.name}.png")

顺手把driver.page_source也存下来,很多问题看源码一眼就明白了。

实操心得:赛场上如果时间紧,先把“能跑通”的版本保存一份备份,再做优化。我见过有人改重构改到跑不通,又没备份,最后交了个半成品。留一手,比什么都强。


4. 接口测试题:别只盯着 200

接口模块是这几个模块里“投入产出比”最高的——不需要处理页面渲染、不需要等待元素、跑起来飞快,只要 HTTP 功底扎实,分很好拿。但它也是最容易“自我感觉良好”的模块,因为很多人把“状态码 200”当成了测试通过的唯一标准,实际上 200 只说明服务没崩,业务对不对完全是另一回事。

模拟赛的接口题通常给一份接口文档(或者让你自己抓包),要求你对若干接口做测试,覆盖正常参数、异常参数、边界值、鉴权缺失等场景,最后输出测试结果。

4.1 抓包和接口清单梳理

如果赛题没给完整接口文档,第一步必然是抓包。浏览器 F12 打开 Network 面板,勾选 Preserve log,先把主流程走一遍,然后按 XHR/Fetch 过滤,把所有业务接口列出来。列的时候不能只看 URL,要记录四样东西:请求方法、请求头(重点看 Content-Type 和鉴权字段)、请求体结构、响应体结构。

整理成清单之后,你才能看出接口之间的依赖关系。比如登录接口返回 token,后续所有接口都要带这个 token;查询列表接口返回 id,详情接口要用这个 id。这个依赖关系直接决定了你的脚本结构——先调 A,从响应里提取字段,再传给 B。

这里有个经常被忽略的点:抓包时要看响应体里的业务码。很多系统 HTTP 状态码永远是 200,真正的结果放在{"code": 0, "msg": "成功", "data": {...}}里。你只断言 200,等于什么都没断言。

4.2 请求构造与三层断言

我用 requests 写脚本时,习惯做一个 session 级别的封装,把 base_url、超时、公共头统一处理:

import requests class ApiClient: def __init__(self, base_url, timeout=5): self.s = requests.Session() self.base = base_url.rstrip("/") self.s.headers.update({ "Content-Type": "application/json", "User-Agent": "autotest/1.0", }) self.timeout = timeout def request(self, method, path, **kwargs): url = f"{self.base}{path}" kwargs.setdefault("timeout", self.timeout) resp = self.s.request(method, url, **kwargs) return resp

断言我分三层写,缺一层都算不完整:

def test_login_wrong_password(client): resp = client.request("POST", "/api/login", json={"username": "tester", "password": "wrong"}) body = resp.json() # 第一层:HTTP 层 assert resp.status_code == 200 # 第二层:业务码层 assert body["code"] == 1001 # 第三层:提示文案层(用 in 而不是 ==,避免文案微调导致误报) assert "密码错误" in body["msg"] # 补充:响应结构完整性 assert "data" in body

第三层用in而不是==是有讲究的。文案经常会微调,比如“密码错误”变成“用户名或密码错误”,用等号断言会误报,用包含判断更稳。但也不能太松,包含的关键词要能唯一标识这个错误类型。

参数校验类的用例,我一般用参数化批量跑:

import pytest @pytest.mark.parametrize("payload,expect_code", [ ({"username": "", "password": "123456"}, 1002), # 用户名为空 ({"username": "tester", "password": ""}, 1002), # 密码为空 ({"username": "a"*65, "password": "123456"}, 1002), # 用户名超长 ({"username": "tester", "password": "123456"}, 0), # 正常 ]) def test_login_params(client, payload, expect_code): resp = client.request("POST", "/api/login", json=payload) assert resp.json()["code"] == expect_code

4.3 鉴权、幂等与异常场景

这三块是拉开差距的地方。

鉴权测试:不带 token、带过期 token、带伪造 token、用 A 用户的 token 访问 B 用户的数据(越权)。最后这个特别重要,赛题经常埋这个点。验证方法很简单:登录两个账号,拿 A 的 token 去请求 B 的资源 id,如果返回 200 且带回了 B 的数据,就是水平越权漏洞。

幂等测试:同一个请求连续发两次,看数据是否重复创建。比如提交订单接口,连点两次是否生成两个订单。这个点用脚本很容易验:发两次,然后查询列表,看数量是 1 还是 2。

异常场景:Content-Type 传错(用 form 传 JSON)、请求体不是合法 JSON、必填字段缺失、字段类型错误(价格传字符串)、超长字段、特殊字符和 SQL 关键字(' OR '1'='1,只做输入校验层面的验证,不做任何攻击性尝试)。这些测的是服务端的健壮性,正常系统应该返回明确的错误码而不是 500。

注意:异常测试要把握好边界。验证输入校验是否生效是正常测试行为,但不要做任何可能影响系统稳定性的操作,比如大量并发写、构造恶意 payload。赛场上一切以“证明功能行为是否符合预期”为目的,克制很重要。

4.4 接口模块的常见问题排查

现象可能原因排查动作
401/403token 未带、格式错、已过期打印请求头,确认Authorization格式;重新登录换新 token
400请求体格式与 Content-Type 不匹配用抓包对比原始请求,逐字段比对
返回乱码响应未指定编码resp.encoding = 'utf-8'后再取resp.text
断言一直失败但手工能过参数顺序、字段名大小写、空值处理差异打印实际响应体全文,与预期逐字段对照
依赖接口取不到字段JSON 路径写错、返回结构是列表先print(resp.json())看清结构再取

最后一条我要多说一句。很多人取 token 写成resp.json()["token"],实际结构是{"data": {"token": "xxx"}},直接 KeyError。养成习惯:新接口先打印一次完整响应,看清楚结构再写提取逻辑,这个动作花 10 秒,能省 10 分钟。


5. 性能测试题:线程数不是拍脑袋定的

性能模块是很多人心里的“玄学”,觉得压测结果飘忽不定、不好把握。其实性能测试的脚本部分非常标准化,难点在于场景建模和结果解读——也就是“压多少”和“压完怎么看”。模拟赛的题目一般会给一个明确的性能指标,比如“某查询接口在 50 并发下平均响应时间不超过 500ms,错误率低于 1%”,你要做的就是搭出这个场景并证明它达标或者不达标。

5.1 线程数、Ramp-up、循环次数怎么算

这是必须讲清楚的部分,因为它是“有据可循”的,不是凭感觉。

先说并发数的估算。最常用的是利特尔法则(Little's Law):并发数 = 目标 TPS × 平均响应时间(秒)。举个例子,业务目标是这个接口每秒处理 50 个请求,实测单请求平均响应 0.2 秒,那么需要的并发线程数大约是 50 × 0.2 = 10 个。考虑网络抖动和系统波动,一般再加 20%~30% 缓冲,实际设 12~13 个线程。

如果题目直接给了“模拟 100 个用户同时在线”,那就不能用利特尔法则了,直接设 100 线程,但要考虑 Ramp-up。Ramp-up 是“多少秒内把线程全部启动”,设得太小(比如 100 线程 1 秒启动)会造成瞬时冲击,曲线难看;设得太大(100 线程 100 秒启动)又压不到峰值。经验值是Ramp-up ≈ 线程数 / 10 到 线程数 / 5,100 线程就设 10-20 秒,让压力平滑上升。

循环次数怎么定?如果要做持续时间型测试(比如持续压 5 分钟),用“调度器”配置持续时间,循环次数勾选“永远”;如果要做固定请求量测试(比如总共发 1000 个请求),循环次数 = 1000 / 线程数。

我把常见的参数组合整理成表:

测试目标线程数Ramp-up循环/调度说明
基准测试(单用户)11循环 10 次拿基线响应时间
负载测试(目标 TPS)TPS × RT × 1.2线程数/10持续 5 分钟验证是否达标
并发测试(模拟 N 用户)NN/10持续 3-5 分钟看高并发下是否劣化
稳定性测试峰值 70%30 秒持续 30 分钟+看内存/连接是否泄漏

提示:Ramp-up 不要设成 0(JMeter 不允许),也不要设成 1。1 秒启动 100 线程那种“瞬时冲击”测出来的数据没有参考价值,因为真实用户不会在同一毫秒涌入。

5.2 脚本搭建的关键元件

JMeter 脚本我一般按这个顺序搭:测试计划 → 线程组 → HTTP 请求默认值 → HTTP 信息头管理器 → 事务控制器 → 具体请求 → 断言 → 监听器。

HTTP 请求默认值里填服务器 IP、端口、协议、编码(UTF-8),这样每个请求只写路径,改环境时只改一处。信息头管理器里放Content-Type: application/json和鉴权头。

如果需要先登录拿 token 再压业务接口,就要用JSON 提取器做关联:

Names of created variables: token JSON Path expressions: $.data.token Default Values: NOT_FOUND

然后在后续请求的头管理器里引用${token}。这里有个细节:提取器的作用域要设对,放在登录请求的子节点下,否则可能取到空值。取不到的时候,加一个调试取样器(Debug Sampler)把变量打出来看,比猜快得多。

断言我至少加两层:响应断言(状态码 200)+持续时间断言(比如要求 500ms 内,就设 Duration Assertion 500ms)。持续时间断言很关键,它能把“慢请求”直接标记为失败,报告里一眼看出来。

监听器方面,聚合报告(Aggregate Report)和察看结果树(View Results Tree)是必须的,但压测时要把结果树关掉,它会把每个请求的完整响应存内存,几百线程跑几分钟就 OOM 了。需要看单条请求细节时再临时打开。

5.3 聚合报告怎么读才有说服力

聚合报告的字段含义必须搞清楚,不然写出来的分析就是废话:

字段含义关注点
Average平均响应时间参考价值有限,容易被极端值拉偏
Median中位数,50% 请求快于此值比平均值更能反映典型体验
90% Line90% 请求快于此值核心指标,反映大多数用户感受
95% Line / 99% Line长尾指标看极端慢请求
Min / Max最快/最慢Max 异常高说明有卡顿
Error %错误率超过 1% 就要警惕
Throughput吞吐量(请求/秒)与目标 TPS 对比

写分析结论的时候,不要只贴图。我见过太多报告就一张聚合报告的截图,然后写“性能良好”。这种报告拿不到分,因为没有任何解读。正确的写法是:先给指标,再给判断,最后给原因推测。比如:

在 50 并发、持续 5 分钟的场景下,查询接口平均响应 186ms,90% Line 为 312ms,最大响应 890ms,错误率 0.02%,吞吐量 268 请求/秒。90% Line 远低于 500ms 的目标,判断达标。最大响应时间出现在压测开始后约 30 秒,推测与连接池预热和 JIT 编译有关,属于正常现象,随后曲线趋于平稳。测试期间未观察到错误率随压力上升而增长,说明系统在该压力下稳定。

这段话里既有数据,又有对比目标的判断,还有对异常点的解释,这才叫“分析”。评委看的就是这个。

5.4 性能模块的坑

坑一:压测机自己成了瓶颈。GUI 模式跑高并发,JMeter 自己就把 CPU 吃满了,测出来的响应时间包含本机渲染开销,数据不准。规范做法是用非 GUI 模式跑:jmeter -n -t plan.jmx -l result.jtl -e -o report/,跑完生成 HTML 报告。命令行跑出来的数据干净得多。

坑二:没有预热。前几十个请求往往特别慢(连接建立、缓存未命中),直接算进平均值会拉高数据。做法是先跑 30 秒预热,然后清空结果再正式统计,或者在报告里把前 30 秒的数据单独说明。

坑三:断言缺失导致错误率虚低。业务失败但 HTTP 返回 200,没断言就统计不出来,错误率显示 0%,实际上大量请求业务失败。所以必须加响应断言,检查返回内容里的业务码。

坑四:忽略被测系统的数据量。在只有 10 条记录的库上压查询,跟在 10 万条记录上压,结果天差地别。如果赛题允许,提前把数据造到有代表性的量级;如果不允许,在报告里注明数据规模,说明结论的适用范围。


6. 单元测试与白盒:覆盖率不是越高越好

白盒/单元测试模块的分值通常不高,但它有个特点——评分标准非常客观,行覆盖率、分支覆盖率是机器算出来的,不掺主观因素。所以这块是“该拿的分必须拿满”的类型。

模拟赛一般给一段 Java 或 Python 的业务代码(比如一个计算器类、一个订单金额计算函数、一个字符串处理工具),要求你写单元测试,达到指定的覆盖率,并且测试用例要能发现代码里的问题。

6.1 从代码结构反推测试用例

拿到代码,我的顺序是:先读方法签名和分支,再列测试点,最后写代码。

以一段“计算订单最终金额”的代码为例,逻辑大概是:基础金额减去优惠,如果会员等级是金卡再打 9 折,如果金额超过 500 免运费,否则运费 10 元,最终金额不能为负。这种代码里有四个分支条件,两两组合就是 16 种情况,但大部分可以合并。

我在纸上先画一张分支条件表:

分支条件测试数据
B1金卡 且 金额≤500等级=金卡,金额=100
B2金卡 且 金额>500等级=金卡,金额=600
B3非金卡 且 金额≤500等级=普通,金额=100
B4非金卡 且 金额>500等级=普通,金额=600
B5优惠后金额 < 0优惠=1000,金额=100
B6金额正好 = 500(边界)金额=500
B7金额 = 0(边界)金额=0

这七组数据能把所有分支都覆盖到。写的时候用参数化,一组数据一行,报告清晰。

Python 版本:

import pytest from order import calc_final_amount @pytest.mark.parametrize("level,amount,coupon,expect", [ ("gold", 100, 0, 90), # 金卡9折,加运费10 ("gold", 600, 0, 540), # 金卡9折,免运费 ("normal", 100, 0, 110), # 普通,加运费 ("normal", 600, 0, 600), # 普通,免运费 ("normal", 100, 1000, 0), # 金额为负,兜底为0 ("normal", 500, 0, 510), # 边界:正好500,仍收运费 ("normal", 0, 0, 10), # 边界:0元订单 ]) def test_calc_final_amount(level, amount, coupon, expect): assert calc_final_amount(level, amount, coupon) == expect

Java 版本用 JUnit 5 的@ParameterizedTest加@CsvSource,思路一样。注意金额用BigDecimal的时候,断言要用compareTo而不是equals,因为1.0和1.00的 scale 不同,equals会返回 false,这是个经典坑。

6.2 覆盖率报告怎么看

Python 用pytest --cov=src --cov-report=html,Java 用 JaCoCo 插件生成报告。报告里重点看三个指标:

  • 行覆盖率(Line Coverage):执行到的代码行占比
  • 分支覆盖率(Branch Coverage):if/else 两个方向都走到的占比
  • 未覆盖行列表:直接点进去看哪些行是红的

我要说的是:别为了刷覆盖率写无意义的断言。比如assert result is not None这种,行覆盖率上去了,但一行代码都没验证对错。评分细则里如果写了“覆盖率 + 断言有效性”,这种写法反而扣分。

更聪明的做法是先看未覆盖行,再针对性补用例。跑一次覆盖率,红色行就是你漏掉的逻辑分支,通常都是异常路径(除零、空值、越界),补上这些,覆盖率自然上去,而且用例质量高。

6.3 单元测试的注意事项

第一,测试方法要独立。不能 A 测试依赖 B 测试创建的对象,每个测试方法自己造数据。用@BeforeEach/ fixture 做初始化。

第二,处理好外部依赖。如果被测代码里有数据库查询、时间获取、随机数,要 Mock 掉,否则测试不稳定。Python 用unittest.mock.patch,Java 用 Mockito。比如时间相关的逻辑,Mock 成固定时间才能断言结果。

from unittest.mock import patch from datetime import datetime @patch("order.datetime") def test_expired_coupon(mock_dt): mock_dt.now.return_value = datetime(2024, 1, 1) assert is_coupon_valid("20230101", "20231231") is False

第三,异常路径要单独断言。用pytest.raises或 JUnit 的assertThrows,而且最好断言异常信息,不只是异常类型。

def test_divide_by_zero(): with pytest.raises(ValueError, match="除数不能为0"): divide(10, 0)

第四,命名要能自解释。test_1、test_2这种名字,报告里根本看不出测的是什么。用test_金卡用户_金额600_免运费_实付540这种,虽然长,但一眼就懂。


7. 时间分配和临场取舍

这个部分没有任何文档会写,但它是决定你能不能把所有模块都交上去的关键。模拟赛一般给 3-4 小时,六个模块平均下来每个不到 40 分钟,实际上不可能平均分配。

我的分配思路是先拿确定性高的,再啃耗时的。功能测试的用例设计和缺陷单,只要你方法熟,40 分钟足够,而且分值稳定;自动化脚本如果环境没问题,45 分钟能跑通;接口测试 30 分钟;性能脚本 30 分钟加 15 分钟跑测试;单元测试 25 分钟;剩下 20 分钟做提交前的检查。

但真实情况往往不按计划走。我给你几条临场判断的准则:

卡住超过 15 分钟就换赛道。比如自动化脚本的元素死活定位不到,别死磕,先保存现状,去做接口和单元测试,这两块不依赖浏览器,稳。回头有时间再回来查。我在实战里见过有人在一个定位器上耗了一小时,最后三个模块没做。

性能测试的脚本可以先搭骨架再填肉。线程组、请求、断言先搭好,能跑通一次就行,压测数据可以边跑边写报告。因为压测本身要跑几分钟,这段时间你可以干别的。

报告类内容最后统一写。测试计划、测试总结这类文档,不要一开始写,等所有模块都做完,你才有素材。写的时候直接引用各模块的数据,效率高得多。

提交物的命名和目录结构提前定好。我习惯用模块名_内容类型的命名,比如功能测试_测试用例.xlsx、功能测试_缺陷报告.xlsx、自动化测试_脚本/、接口测试_集合.json、性能测试_报告.pdf。提交前对照题目要求逐条核对,别因为文件名不对被判“未提交”。

提示:比赛结束前 10 分钟,停止一切“优化”,只做检查。检查项包括:文件是否都能打开、脚本是否能在干净环境跑一次、报告里的截图路径是否正确、有没有把临时文件混进去。这几分钟的收益比再写十条用例高得多。

最后说一个我自己踩过的坑:别把测试数据留在被测系统里。有一次我压测造了几千条脏数据,导致后面别人的功能测试用例跑不过,虽然不影响我的分,但如果评分是共用系统,脏数据可能影响你自己后续模块的验证。压测前想好数据怎么清,或者用独立的测试账号和标识。


8. 常见问题速查表

把前面散落的坑集中一下,赛场上遇到问题可以先扫这张表:

现象大概率原因快速处置
浏览器起不来,报驱动版本不匹配Chrome 与 driver 大版本不一致换匹配版本,或脚本里显式指定 Service 路径
元素定位报 NoSuchElement未加等待、在 iframe 内、动态加载加显式等待;switch_to.frame;确认是否在新窗口
点击报 ElementClickIntercepted被浮层/固定头部遮挡先 scrollIntoView 再点,或用 JS 点击
接口 401token 缺失/过期/格式错打印请求头核对,重新登录
接口断言失败但手工能过字段名大小写、嵌套层级、编码print(resp.text)看原始返回
JMeter 跑高并发报 OOMGUI 模式 + 结果树全开改非 GUI 模式,关闭结果树,调大 HEAP
压测错误率异常高断言过严、服务端限流、连接数超限放松断言看真实响应;看服务端日志
单元测试覆盖率上不去异常分支未覆盖看覆盖率报告红行,针对性补用例
单元测试金额断言失败BigDecimal 用 equals 比较 scale改用 compareTo
报告提交后打不开相对路径失效、文件损坏用绝对路径引用截图,提交前逐个打开验证

这张表我会打印出来贴在显示器边上,赛前过一遍,赛中当查询手册。

再补几个“非典型”的排查技巧。接口测不通的时候,先用 curl 发一遍,排除代码封装问题;自动化跑不稳的时候,把driver.implicitly_wait全部删掉,只用显式等待,因为隐式等待和显式等待混用会导致等待时间不可预期;覆盖率突然下降的时候,检查是不是有测试方法被跳过了,@Ignore或者 fixture 出错都会静默跳过。

还有一个策略层面的建议:赛题里如果给了示例或者模板,一定要用。有些人觉得模板约束太多,自己另起炉灶,结果格式不符合评分标准。模板本身就是评分依据的一部分,老老实实按模板填,是最省事的选择。


说点个人体会。这个赛项的题目设计有个很明显的特点:它不追求“难题”,追求“完整度”。单看任何一个模块,难度都不高,会点 Python、会点工具就能上手。但要求在时间压力下把所有模块都交出一份及格线以上的东西,这就筛掉了一大批人。我见过技术很扎实的朋友,因为时间没规划好,最后只交了三个模块;也见过技术一般但流程走得很顺的人,总分反而更高。

所以我一直觉得,准备这个比赛最有效的方式不是刷题,而是完整地做一次全流程演练——找个开源的 Demo 系统,从需求分析开始,走完用例设计、自动化、接口、压测、单元测试、报告,掐着表做一遍。做完这一遍,你对“哪一步会卡多久”“哪个模块该先做”就有直觉了。这种直觉在赛场上比任何技巧都值钱。

另外就是心态。模拟赛的意义本来就是暴露问题,题目解析看再多的“标准答案”,不如自己动手把每一个坑踩一遍。我现在回头看自己第一次做这类题目的记录,发现当时觉得“莫名其妙”的那些扣分点,其实每一条都在提示同一个道理:测试工作的价值不在于你跑通了多少,而在于你能让多少人看懂你验了什么、怎么验的、结果意味着什么。把这个想明白了,题目本身反而不难了。

返回列表