003:Coding与AI编程
Coding 能力与 AI-Coding 专题(手撕算法/测开脚本题/Review AI代码/Prompt指挥)
📑 目录
- 1 Coding能力与AICoding专题
- 2 第二部分:刷题清单
- 2.1 传统手撕算法高频清单(LeetCode 题号)
- 2.2 测开特有手写脚本题(不是 LeetCode,面试白板手写)
- 2.3 刷题优先级(测开)
- 3 第三部分:AI-Coding 模拟面试样题
- 3.1 题型 1:Review AI 生成的自动化代码(最高频)
- 3.2 题型 2:你如何写 Prompt 指挥 AI 完成测开任务
- 3.3 题型 3:AI 生成测试用例,你来评审
- 3.4 题型 4:真实工作场景问答(口头)
- 3.5 面试实操建议
- 3.6 备考优先级排序
- 4 第四部分:面试口述完整模板(2 套高频真题)
- 4.1 题目 1:Review AI 生成的接口自动化代码(高频真题)
- 4.2 题目 2:口头题:AI 生成代码跑不起来,你的排查步骤?(AI-coding 必考口头题)
- 4.3 题目 3:Prompt 设计题(面试口述)
- 4.4 💡 面试小提示
- 5 第五部分:测开手撕真题口述模板(初级)
- 5.1 题 1:LC56 合并区间(测开高频,模拟测试时间段合并、用例时间窗口)
- 5.2 题 2:手写日志解析脚本(测开特有手写题,面试超级高频)
- 5.3 题 3:JSON 字典 Diff 对比(测开面试超级高频)
- 5.4 题 4:简易重试装饰器(测开面试高频手写脚本题)
- 5.5 一页纸速记版(含前面 4 道工程题 + 基础题)
- 6 第六部分:高级测开社招(8-12 年)面试考察
- 6.1 核心特征
- 6.2 LeetCode 真题(会出现的,中等为主)
- 6.3 高级测开社招:工程手写编码题(10 年社招重点,比 LeetCode 占比更高)
- 6.4 大模型 / AI 测试方向高级测开额外考察(字节、阿里、百度、部分 AI 公司)
- 6.5 10 年社招算法 / 编码面试非常重要的面试要点(加分项)
- 6.6 10 年测开社招面试的现实情况总结
- 6.7 📌 备考优先级(10 年高级测开)
- 7 第七部分:手撕代码全合集(完整代码 + 逐行注释)
- 7.1 第一部分:校招 / 1-3 年社招基础
- 7.2 第二部分:8-12 年高级测开 / 测试架构社招题
- 8 附录 A:AI-Coding 面试题(无代码,面试口述)
- 9 附录 B:面试编码通用答题模板
- 10 开发工具与工程生态(TOML/YAML/Poetry/Textual/Qodo)
- 10.1 TOML 与 YAML 的区别(J21)
- 10.2 Poetry 使用与虚拟环境管理(J20)
- 10.3 Textual 终端可视化面板(TUI 实战)(H11)
- 10.4 Qodo:AI 代码测试工具(H14)
1 Coding能力与AICoding专题
体系定位:Coding 能力与 AI-Coding 专题(手撕算法/测开脚本题/Review AI代码/Prompt指挥) 来源文件数:8(内容原封不动按来源保留)
📄 来源:003_测试开发面试全合集.md(# 第二部分:刷题清单)
2 第二部分:刷题清单
2.1 传统手撕算法高频清单(LeetCode 题号)
说明:测开算法不考复杂 DP、高级图论,偏向工程、数据处理、字符串、日志解析,重点考察边界、异常 case。
2.1.1 🔹 简单-中等(面试最高频,优先刷)
字符串处理(测开超级高频,日志、报文解析) - LC3 无重复字符的最长子串 - LC125 验证回文串 - LC541 反转字符串 II - LC151 反转字符串里的单词(日志字符串切分高频)
哈希表(统计、去重、接口结果对比) - LC1 两数之和 - LC49 字母异位词分组 - LC217 存在重复元素 - LC349 两个数组的交集
链表(基础逻辑校验) - LC206 反转链表 - LC876 链表的中间结点 - LC141 环形链表
双指针 / 二分查找 - LC27 移除元素 - LC35 搜索插入位置 - LC611 有效三角形的个数(可选)
数组、工程脚本类(模拟真实测开工作) - LC88 合并两个有序数组 - LC56 合并区间(处理时间段、测试用例区间)
2.1.2 🔹 笔试 OA 容易遇见
- LC14 最长公共前缀
- LC20 有效的括号
✅ 测开避坑:Hard 题几乎不考;DP、拓扑排序、复杂图题不用花大量时间。
2.2 测开特有手写脚本题(不是 LeetCode,面试白板手写)
这一类比纯算法更常考,是测开特色:
- 手写函数:解析一段 http 日志字符串,提取出全部请求 url、状态码,统计每个状态码出现次数
- 手写函数:对比两个 json 字典,输出 key 不一样、value 不一致的 diff 差异集合
- 手写简易重试装饰器:对接口请求函数,失败自动重试 N 次,超时抛出异常
- 手写读取 csv,过滤异常行,输出过滤后的结果
2.3 刷题优先级(测开)
- 工程脚本 4 道:日志解析、csv 过滤、json-diff、重试装饰器(测开最高频)
- 简单算法:两数之和、反转链表、回文串、合并区间
- 不要死磕 hard DP、复杂图论。
📄 来源:003_测试开发面试全合集.md(# 第三部分:AI-Coding 模拟面试样题)
3 第三部分:AI-Coding 模拟面试样题
核心考察点:不是让 AI 写完交差,考察你拆解需求、写 prompt、识别 AI 幻觉、review 代码、补充测试 case。 面试形式:面试官给需求 / 给一段 AI 生成的有 bug 代码,你来处理。
3.1 题型 1:Review AI 生成的自动化代码(最高频)
面试官给下面这段 AI 产出接口自动化代码,提问:找出代码中所有缺陷、漏洞,并且修复。
# AI生成:查询用户接口自动化用例
import requests
def get_user_info(user_id):
resp = requests.get(f"/api/user/{user_id}")
data = resp.json()
return data["name"]
# 执行用例
res = get_user_info(None)
print(res)
参考答案要点(面试口述): 1. 缺少 base_url,只有路径,请求会直接报错; 2. 没有捕获网络异常:超时、连接失败完全没处理; 3. 没有判断 http 状态码,404/500 依然直接 resp.json(),会抛异常; 4. user_id 传入 None,拼接 url 会生成非法 url; 5. data["name"] 直接取 key,如果 key 不存在直接抛 KeyError,没有容错; 6. 没有用例断言逻辑,没有测试结果输出。
3.2 题型 2:你如何写 Prompt 指挥 AI 完成测开任务
面试问题:我们需要写一个接口自动化脚本,对 /api/order/list 订单列表接口做基础自动化,需要校验返回字段、校验分页,你会给大模型写什么样的 prompt?输出你实际会写的 prompt,并且说明 AI 输出之后你要做哪些校验。
参考 Prompt 示例(面试口述):
请使用python+requests编写接口自动化脚本:
接口地址GET /api/order/list
入参:page_num页码,page_size每页条数
需求:
1. 正常场景:传入合法page_num=1,page_size=10,请求接口
2. 需要校验http 200;校验返回json包含code、data、total、list字段
3. 校验list返回条数不大于page_size
4. 增加异常case:page_num传负数,校验接口返回错误码
5. 增加超时处理,超时时间5秒
6. 不要使用任何第三方测试框架,原生python实现,输出清晰的执行结果。
输出完成后,请你帮我列出这份脚本潜在风险点。
后续校验步骤(必说): - 检查 AI 有没有漏掉异常场景; - 检查硬编码问题、没有做参数化; - 自己构造边界用例执行,验证脚本是否真的能复现; - AI 经常幻觉编造接口字段,必须对照真实接口文档核对。
3.3 题型 3:AI 生成测试用例,你来评审
业务背景:用户注册接口,手机号 + 密码注册。AI 输出一批测试用例,请你指出缺失哪些 case,哪些 case 不合理。
AI 输出用例: - 正确手机号 + 正确密码,注册成功 - 手机号为空注册失败 - 密码为空注册失败
👉 面试回答要点:缺失:手机号格式非法、手机号已注册、密码过短、密码超长、特殊字符、边界长度、并发场景。
3.4 题型 4:真实工作场景问答(口头)
- "工作中你大量使用 AI 生成自动化脚本,如果 AI 写出来代码跑不起来,你的排查步骤是什么?"
- "AI 经常会编造接口字段、编造不存在的函数,在测试开发工作中,你如何规避 AI 幻觉带来的线上漏测风险?"
3.5 面试实操建议
- 笔试阶段(OA):绝大多数公司禁止使用 AI,切屏检测,不要冒险。
- 现场 / 线上面试编码:
- 面试开始可以主动反问:"coding 环节是否允许使用 AI 辅助工具?"
- 面试 AI-coding 类题目,千万不要说"直接交给 AI 全部搞定",面试官想听到:AI 是辅助,人做评审、校验、补边界。
3.6 备考优先级排序
- 先搞定:字符串、哈希、数组双指针手撕算法 + 测开手写脚本题;
- 再练习:Review 有 bug 的 AI 代码、自己写测开场景 prompt;
- 口头能讲清楚 AI 辅助测试的风险点。
📄 来源:003_测试开发面试全合集.md(# 第四部分:面试口述完整模板(2 套高频真题))
4 第四部分:面试口述完整模板(2 套高频真题)
场景:线上面试,面试官把代码 / 问题抛出来,你口头回答,语速适中,逻辑分层,先说现象、再说风险、最后给出修复方案。
4.1 题目 1:Review AI 生成的接口自动化代码(高频真题)
给到的 AI 错误代码:
import requests
def get_user_info(user_id):
resp = requests.get(f"/api/user/{user_id}")
data = resp.json()
return data["name"]
res = get_user_info(None)
print(res)
🎤 面试口述完整回答(可以直接照着说):
"我来逐一看这段代码,整体逻辑想实现获取用户信息,但是存在多处健壮性、异常处理、工程上的问题,我分点梳理:
第一点,请求地址不全。 这里只写了接口路径 /api/user/{user_id},没有 base 域名,requests 直接调用会直接报连接错误,无法发送网络请求。
第二点,缺少网络异常捕获。 没有处理超时、连接失败、网络断开这类异常。一旦网络波动,整个脚本直接崩溃退出。
第三点,没有校验 HTTP 响应状态码。 不管接口返回 404、500、401,代码都会直接执行 resp.json()。如果返回不是 JSON 格式,这一步会直接抛出解析异常。
第四点,入参没有做合法性校验。 调用的时候传入 user_id=None,字符串拼接之后 url 会变成非法地址,会引发请求异常。
第五点,字典取值没有容错。 直接 data["name"] 硬编码取 key。如果接口返回数据结构变更,或者这个字段不存在,直接抛出 KeyError,脚本直接中断。
第六点,缺少测试断言。 这个函数只做数据获取,没有结果断言,没有用例结果输出,无法判断用例是成功还是失败。
接下来我说修复思路: 1. 补充完整 base_url; 2. requests 请求增加 timeout 超时时间; 3. 使用 try-except 捕获网络相关异常; 4. 判断 response.status_code,非 200 直接抛出异常; 5. 入参提前校验,user_id 为空、非法直接提前拦截; 6. 字典取值优先用 .get() 做容错,增加字段存在性校验; 7. 增加断言逻辑,输出用例执行结果。"
✨ 修复后参考代码(面试可以贴出来):
import requests
BASE_URL = "https://xxx.com"
def get_user_info(user_id):
# 入参校验
if user_id is None or not isinstance(user_id, (int, str)):
raise ValueError("非法user_id")
url = f"{BASE_URL}/api/user/{user_id}"
try:
resp = requests.get(url, timeout=5)
except requests.exceptions.RequestException as e:
raise RuntimeError(f"网络请求异常: {e}")
if resp.status_code != 200:
raise RuntimeError(f"接口异常,status:{resp.status_code}")
data = resp.json()
name = data.get("name")
if name is None:
raise AssertionError("返回结果缺失name字段")
return name
if __name__ == "__main__":
try:
result = get_user_info(1001)
print(f"用例执行成功,name={result}")
except Exception as e:
print(f"用例执行失败:{e}")
4.2 题目 2:口头题:AI 生成代码跑不起来,你的排查步骤?(AI-coding 必考口头题)
🎤 面试口述标准答案:
"在我的工作流程里面,AI 是辅助工具,不会直接把 AI 输出直接上线,遇到代码跑不通,我会按下面步骤排查:
先区分是环境问题,还是代码逻辑问题。 首先确认依赖包是否齐全、Python 版本、网络环境,很多时候不是代码 bug,是本地环境缺少库、网络不通。
核对 AI 是否产生幻觉。 重点核对:接口 URL、请求方法 GET/POST、请求头、请求参数、返回字段。大模型经常编造接口字段、编造不存在的入参,我会拿接口文档做逐项比对。
最小化复现,拆分定位错误点。 把大段 AI 代码拆分,先单独跑请求部分,看接口原始 response 打印出来,确认原始返回是什么。很多时候 AI 直接假设返回格式,和真实接口不一致。
检查异常分支是否遗漏。 看 AI 有没有处理超时、异常 http 状态码、json 解析失败、key 不存在的场景。AI 写代码经常只写正常流程,忽略异常 case。
边界用例人工补充。 AI 容易漏掉边界场景,比如入参为 0、null、超长字符串,我会手动构造边界 case 去验证脚本。
代码评审,确认业务逻辑正确性。 就算代码跑通了,也要看业务逻辑是否符合测试需求,断言是否完备,不能只看脚本不报错就认为用例没问题。
最后,把定位出来的问题,再喂回 AI,让它针对性修复,而不是一次性让 AI 重写全部代码。"
4.3 题目 3:Prompt 设计题(面试口述)
面试官提问:需要写 /api/order/list 订单列表接口自动化,你会写什么 prompt 给大模型?AI 输出之后你做哪些校验?
🎤 口述模板:
"我的 prompt 会把接口信息、入参、需要覆盖的场景、约束条件全部写清楚,不会只简单说帮我写自动化脚本。 我的 prompt 内容大致是:
使用原生Python+requests编写接口自动化脚本,不要引入pytest等第三方测试框架。
接口:GET /api/order/list
入参:page_num 页码,page_size每页条数
需求:
1. 正常场景:page_num=1,page_size=10发起请求;校验http 200;校验返回json包含code、data、total、list字段;校验list内返回条数不大于page_size。
2. 异常场景:传入负数page_num,校验接口返回业务错误码。
3. 请求设置5秒超时。
4. 脚本需要打印清晰的执行成功/失败结果。
5. 脚本结束输出这份代码潜在风险点。
AI 输出完脚本之后,我会做几层校验: 1. 核对接口文档,检查 AI 有没有幻觉编造字段、参数; 2. 检查异常场景有没有真正实现,有没有只写正常流程; 3. 检查是否硬编码,有没有做参数化; 4. 本地实际运行脚本,用真实接口验证; 5. 自己额外补充 AI 容易漏掉的边界 case,比如 page_size 超大、page_num 超大的场景。"
4.4 💡 面试小提示
- 回答的时候不要一上来直接贴代码,先口头讲思路,再给出代码。
- AI-coding 题目千万不要表达:"全部交给 AI 做",面试官核心考察:人如何把控风险,识别 AI 的错误。
- 测开面试非常看重健壮性、异常、边界 case,回答的时候多提这几个关键词会加分。
📄 来源:003_测试开发面试全合集.md(# 第五部分:测开手撕真题口述模板(初级))
5 第五部分:测开手撕真题口述模板(初级)
面试场景:在线 coding 白板,面试官给题目,先讲思路,再写代码,再讲边界、复杂度。语言统一 Python。 测开面试看重:边界 case、异常输入、可读性,不追求极致炫技。
5.1 题 1:LC56 合并区间(测开高频,模拟测试时间段合并、用例时间窗口)
题目描述:给出一个区间的集合,请合并所有重叠的区间。
示例:输入 [[1,3],[2,6],[8,10],[15,18]] → 输出 [[1,6],[8,10],[15,18]]
🎤 面试口述答题流程(照着说):
"解题思路分为两步: 首先把区间按照每个区间的起始位置排序。只有排完序,我们才可以顺序遍历做合并。 遍历排序后的区间,维护一个结果列表: - 如果结果列表为空,直接把当前区间放进去; - 取出结果列表最后一个区间,比较当前区间 start; - 如果当前区间 start ≤ 最后区间 end → 发生重叠,做合并,更新 end 取两者最大值; - 不重叠,直接 append 进结果。
边界我需要考虑: - 输入为空列表,直接返回空; - 只有一个区间,直接返回原区间; - 区间完全被另一个区间包裹; - 区间刚好相邻,例如 [1,4],[4,5] 也需要合并。
时间复杂度:排序 O(n log n),遍历 O(n);空间 O(n)。"
手写代码:
def merge(intervals):
if not intervals:
return []
# 按区间起点排序
intervals.sort(key=lambda x: x[0])
res = [intervals[0]]
for start, end in intervals[1:]:
last_start, last_end = res[-1]
if start <= last_end:
# 重叠,合并,更新end
res[-1][1] = max(last_end, end)
else:
res.append([start, end])
return res
# 测试用例
print(merge([[1,3],[2,6],[8,10],[15,18]])) # [[1,6],[8,10],[15,18]]
print(merge([])) # []
print(merge([[1,4],[4,5]])) # [[1,5]]
面试补充口述:"这个题在测试开发工作中,比如做测试时长统计、风控时间窗口、压测时间段合并,会遇到类似逻辑。"
5.2 题 2:手写日志解析脚本(测开特有手写题,面试超级高频)
题目描述:给定一段 HTTP 访问日志字符串,每一行格式示例:
127.0.0.1 - - [02/Aug/2026:14:22:10 +0800] "GET /api/user HTTP/1.1" 200 1234
要求写一个函数,输入多行日志文本,完成两件事: 1. 提取每一行的请求 url、http 状态码; 2. 统计每个状态码出现的次数,返回统计字典。 3. 需要处理异常行:日志格式错乱、残缺行直接跳过,不能程序崩溃。
🎤 面试口述答题流程:
"整体思路: - 将输入文本按换行符切分成一行一行; - 逐行遍历每一条日志; - 针对每一行做解析,这里用字符串分割,但是要做异常捕获; - 目标提取:请求 url,状态码。 - 日志格式:引号里面是请求行 "GET /api/user HTTP/1.1",状态码是引号后面紧跟的数字。 - 如果某一行格式不对、解析失败,直接跳过这一行,不抛出异常。 - 使用字典统计各个状态码计数。
重点边界: - 输入为空字符串; - 部分日志行残缺、格式错乱; - 状态码不是数字; - 有空行需要过滤。"
手写代码:
def parse_http_log(log_text: str) -> dict:
stat_count = {}
if not log_text:
return stat_count
lines = log_text.splitlines()
for line in lines:
line = line.strip()
if not line:
continue
try:
# 提取双引号内请求部分
quote_start = line.find('"')
quote_end = line.find('"', quote_start + 1)
if quote_start == -1 or quote_end == -1:
continue
request_part = line[quote_start+1:quote_end]
parts = request_part.split()
if len(parts) < 2:
continue
url = parts[1]
# 引号后面截取状态码
after_quote = line[quote_end+1:].strip()
status_str = after_quote.split()[0]
if not status_str.isdigit():
continue
status_code = int(status_str)
# 统计计数
stat_count[status_code] = stat_count.get(status_code, 0) + 1
except Exception:
# 解析失败直接跳过该行,不崩溃
continue
return stat_count
# 测试样例
log_sample = '''
127.0.0.1 - - [02/Aug/2026:14:22:10 +0800] "GET /api/user HTTP/1.1" 200 1234
127.0.0.1 - - [02/Aug/2026:14:22:11 +0800] "POST /api/order HTTP/1.1" 500 888
bad log line here
127.0.0.1 - - [02/Aug/2026:14:22:12 +0800] "GET /api/list HTTP/1.1" 200 567
'''
result = parse_http_log(log_sample)
print(result)
# 输出 {200:2, 500:1}
面试口述补充:"真实工作中,我们经常分析 Nginx、服务端日志,不能因为一行脏日志导致整个脚本挂掉,所以这里必须加 try-except 容错。如果日志量很大,这个函数也可以改成逐行读取文件,而不是一次性读全部文本。"
5.3 题 3:JSON 字典 Diff 对比(测开面试超级高频)
题目描述(面试原题):写一个 Python 函数,输入两个字典 dict_a、dict_b,代表两份接口返回的 JSON 对象。输出差异结果,区分三类情况:
- only_in_a:只存在 a 里面的 key
- only_in_b:只存在 b 里面的 key
- value_diff:两边都存在,但 value 值不相等的 key,同时给出 a 的值、b 的值
要求:只处理一层字典;兼容数字、字符串、布尔值;输入允许是空字典,不能抛异常;返回 diff 结果字典,方便后续做测试断言。
示例输入:
a = {"name":"zhangsan","age":20,"gender":"male"}
b = {"name":"lisi","age":20,"city":"beijing"}
预期输出:
{
"only_in_a": {"gender":"male"},
"only_in_b": {"city":"beijing"},
"value_diff": {"name":{"a":"zhangsan","b":"lisi"}}
}
🎤 面试口述完整答题流程:
"我的实现思路: - 先拿到两个字典所有 key 集合,利用集合做差集运算,集合操作非常适合做 key 对比。 - only_in_a:a 有、b 没有的 key; - only_in_b:b 有、a 没有的 key; - common_keys:两边共同存在的 key,遍历这些 key 对比 value; - value 不相等,就存入 value_diff,同时保存两边的值; - 边界要考虑:输入是空字典、value 是 bool、数字; - 函数做容错,入参如果不是 dict,做简单保护,避免程序直接崩溃。 - 集合运算时间复杂度 O(n),效率很高,适合接口返回 json 对比,这也是测试开发日常接口断言经常遇到的场景。"
手写代码:
def json_dict_diff(dict_a: dict, dict_b: dict) -> dict:
# 防御校验,防止传入非字典
if not isinstance(dict_a, dict):
dict_a = {}
if not isinstance(dict_b, dict):
dict_b = {}
keys_a = set(dict_a.keys())
keys_b = set(dict_b.keys())
only_in_a_keys = keys_a - keys_b
only_in_b_keys = keys_b - keys_a
common_keys = keys_a & keys_b
only_in_a = {k: dict_a[k] for k in only_in_a_keys}
only_in_b = {k: dict_b[k] for k in only_in_b_keys}
value_diff = {}
for k in common_keys:
va = dict_a[k]
vb = dict_b[k]
if va != vb:
value_diff[k] = {"a": va, "b": vb}
return {
"only_in_a": only_in_a,
"only_in_b": only_in_b,
"value_diff": value_diff
}
# 测试样例
if __name__ == "__main__":
a = {"name":"zhangsan","age":20,"gender":"male"}
b = {"name":"lisi","age":20,"city":"beijing"}
res = json_dict_diff(a, b)
print(res)
# 空字典测试
print(json_dict_diff({}, {}))
输出样例:
{
'only_in_a': {'gender': 'male'},
'only_in_b': {'city': 'beijing'},
'value_diff': {'name': {'a': 'zhangsan', 'b': 'lisi'}}
}
🎯 面试追加追问(面试官大概率会问,附带口述答案)
追问 1:如果 JSON 里面有嵌套字典怎么办?现在这个函数只能处理一层?
口述回答:当前版本只做一层对比。如果需要支持嵌套 JSON,要改成递归实现:遇到 value 还是 dict 的时候,递归调用 diff 函数;但是测试工作中,很多接口自动化场景,我们会提前拍平 JSON,把嵌套 key 变成 user.name 这种扁平 key,再做 diff,避免递归复杂度。
追问 2:列表怎么处理?比如 a:[1,2,3] b:[3,2,1],你认为算相等吗?
口述回答:当前 != 是严格顺序对比。如果业务语义是集合不关心顺序,需要把列表转集合再比较;如果业务关心顺序,直接用 != 判断。在测试工作中,要根据接口文档业务语义选择,不能写死。
追问 3:拿到这个 diff 结果,在测试开发中你会怎么用?
口述回答:拿到 diff 之后: - 如果 only_in_a / only_in_b / value_diff 全部为空,代表两份 JSON 完全一致,用例断言通过; - 如果有差异,把 diff 输出到测试报告,方便开发定位是哪个字段变更; - 还可以过滤掉某些允许变更的字段,比如时间戳、动态 id,过滤之后再断言。
✨ 测开面试答题小技巧 - 优先写单层版本,不要一上来写递归嵌套版本。先把基础功能写对,面试官追问嵌套,再讲递归方案。 - 一定要提到集合做 key 差集,不要写两层 for 循环遍历 key,两层循环时间复杂度高,面试官会扣分。 - 一定要主动提到边界:空字典、入参不是字典,做防御处理。 - 写完代码,口头跑一遍样例,把输出说出来。 - 真实工作场景:接口对比、基准快照对比、线上线下环境返回结果对比,大量用到这个逻辑。
5.4 题 4:简易重试装饰器(测开面试高频手写脚本题)
题目描述(面试高频原题):手写一个 Python 重试装饰器 @retry。需求:
- 被装饰的函数抛出异常的时候,自动重试;
- 支持自定义最大重试次数;
- 支持每次重试之间设置休眠间隔;
- 达到最大重试次数仍然失败,则抛出原始异常;
- 只捕获指定类型的异常,不是捕获所有异常(不能一把 except Exception: 无脑吞全部异常,这是测开面试扣分点);
- 可以打印日志,输出重试次数信息。
示例使用方式:
@retry(max_times=3, sleep_sec=1, catch_exc=(IOError,))
def request_api():
...
🎤 面试口述答题流程:
"这个是接口请求非常常用的工具装饰器,网络抖动、临时接口报错场景会用到。 实现思路: - 装饰器是带参数的装饰器,外层接收 max_times、sleep_sec、catch_exc 这些配置参数; - 内层包装真正业务函数; - 使用循环,循环最多执行【最大重试次数】; - try 执行业务函数:成功直接返回结果; - 如果抛出我们指定捕获的异常,sleep 之后继续循环重试; - 如果不是我们指定捕获的异常,直接抛出,不要乱吞; - 循环耗尽全部重试次数,还是失败,抛出原始异常; - 打印重试日志,方便排查测试脚本执行情况。
边界点我需要考虑: - max_times 不能传负数,做参数校验; - 不能捕获全部异常,只捕获传入指定的异常类型,避免把业务逻辑错误也盲目重试; - 重试耗尽之后,抛出原始异常,而不是吞掉异常; - sleep_sec 等于 0,代表不等待直接重试。"
手写完整代码:
import time
from functools import wraps
def retry(max_times:int, sleep_sec:float, catch_exc:tuple):
"""
:param max_times: 最大重试总次数(包含第一次执行)
:param sleep_sec: 每次重试之间休眠秒数
:param catch_exc: 需要捕获并重试的异常元组,例(requests.exceptions.RequestException,)
"""
# 参数防御校验
if max_times < 1:
raise ValueError("max_times必须大于等于1")
if sleep_sec < 0:
raise ValueError("sleep_sec不能为负数")
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_times):
try:
return func(*args, **kwargs)
except catch_exc as e:
last_exception = e
# 不是最后一次,才sleep重试
if attempt != max_times - 1:
print(f"[retry] 第{attempt+1}次执行失败,即将重试,err:{e}")
time.sleep(sleep_sec)
# 全部重试耗尽,抛出原始异常
raise last_exception
return wrapper
return decorator
# ---------------- 使用示例 --------------
import requests
# 最多重试3次,间隔1秒,只捕获网络请求类异常
@retry(max_times=3, sleep_sec=1, catch_exc=(requests.exceptions.RequestException,))
def call_api():
resp = requests.get("https://example.com/api/demo", timeout=2)
resp.raise_for_status()
return resp.json()
if __name__ == "__main__":
try:
res = call_api()
print("请求成功", res)
except Exception as e:
print("全部重试之后仍然失败", e)
关键点说明:
functools.wraps(func)保留原函数元信息,面试写上会加分;很多面试者写装饰器会漏掉 wraps。
🎯 面试官高频追问 + 口述标准答案
追问 1:为什么不直接写 except Exception,捕获全部异常?
"如果捕获全部异常,像参数错误、断言错误、KeyError 这类业务逻辑错误,也会盲目重试。接口测试场景,只有网络波动、连接超时这类临时异常适合重试;业务逻辑错误重试多少次都不会成功,不应该浪费次数,要直接抛出。所以只捕获传入的指定异常类型。"
追问 2:max_times=3,代表总共执行几次?
"总共执行 3 次:第 1 次正常执行,失败,再重试 2 次。全部失败之后抛出异常。"
追问 3:真实工作中,还可以做哪些增强?
口述回答: - 增加指数退避,sleep 时间逐步变长,而不是固定 sleep; - 增加回调钩子,重试失败可以上报埋点、写入测试报告; - 增加开关,可以关闭重试; - 部分场景某些业务错误码不适合重试,可以增加判断跳过重试。
追问 4:什么场景不适合用重试?
"写操作接口,比如下单、支付,如果接口没有幂等性,重试会造成重复下单,这种场景不能随便加重试装饰器,会产生脏数据。"
✨ 面试答题小提示 - 不要上来直接写代码,先说清楚装饰器分层逻辑; - 重点突出:不吞全部异常、参数校验、耗尽重试抛出原始异常、wraps 保留函数信息,这几个是测开面试官重点打分点; - 主动提业务坑点:非幂等接口慎用重试,这一点非常加分。
5.5 一页纸速记版(含前面 4 道工程题 + 基础题)
核心特点:重边界、重异常容错、工程实用性优先,不追求炫技。
5.5.1 LC1 两数之和(哈希,校招高频)
def two_sum(nums, target):
hashmap = {}
for idx, num in enumerate(nums):
need = target - num
if need in hashmap:
return [hashmap[need], idx]
hashmap[num] = idx
return []
🎤 口述要点:用哈希表存数值-下标,一次遍历,时间 O(n);暴力两层循环 O(n²) 不推荐。边界:没有解返回空列表;同一个元素不能使用两次。测开角度:输入空数组直接返回。
5.5.2 LC206 反转链表(链表基础)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverse_list(head):
prev = None
cur = head
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev
🎤 口述要点:迭代原地反转,不需要额外空间;边界:链表为空、只有一个节点直接返回;社招测开不会考复杂链表题,这个是链表基础门槛。
5.5.3 LC125 验证回文串(字符串高频)
def is_palindrome(s: str) -> bool:
left, right = 0, len(s)-1
while left < right:
while left < right and not s[left].isalnum():
left +=1
while left < right and not s[right].isalnum():
right -=1
if s[left].lower() != s[right].lower():
return False
left +=1
right -=1
return True
🎤 口述要点:双指针,跳过非字母数字字符,统一转小写;边界:空字符串直接返回 True。
5.5.4 读取 CSV,过滤异常行(测开工程手写题)
def read_and_filter_csv(csv_text: str, expect_col_cnt:int):
result = []
if not csv_text:
return result
lines = csv_text.splitlines()
for line in lines:
line = line.strip()
if not line:
continue
parts = line.split(",")
# 字段数量不对直接丢弃这一行
if len(parts) != expect_col_cnt:
continue
result.append(parts)
return result
# 测试
csv_demo = '''
zhangsan,20,beijing
lisi,25,shanghai
badline,,
wangwu,30,guangzhou
'''
print(read_and_filter_csv(csv_demo,3))
🎤 口述要点:真实工作经常处理测试数据 csv;必须跳过空行、脏行,不能一行脏数据导致整个脚本崩溃;如果是真实生产,优先使用 python 标准库 csv 模块,手写版本仅用于面试考察逻辑。
5.5.5 LC56 合并区间
def merge(intervals):
if not intervals:
return []
intervals.sort(key=lambda x:x[0])
res = [intervals[0]]
for start,end in intervals[1:]:
last_s,last_e = res[-1]
if start <= last_e:
res[-1][1] = max(last_e,end)
else:
res.append([start,end])
return res
🎤 口述要点:排序之后遍历;边界:空输入、区间包裹、区间相邻。
5.5.6 HTTP 日志解析脚本(一页纸速记版)
def parse_http_log(log_text: str) -> dict:
stat_count = {}
if not log_text:
return stat_count
lines = log_text.splitlines()
for line in lines:
line = line.strip()
if not line:
continue
try:
qs = line.find('"')
qe = line.find('"',qs+1)
if qs == -1 or qe == -1:
continue
req_part = line[qs+1:qe]
req_items = req_part.split()
if len(req_items) <2:
continue
after = line[qe+1:].strip()
status_str = after.split()[0]
if not status_str.isdigit():
continue
sc = int(status_str)
stat_count[sc] = stat_count.get(sc,0)+1
except Exception:
continue
return stat_count
🎤 口述要点:脏日志行直接跳过,不抛异常。
5.5.7 JSON 字典 diff(单层速记版)
def json_dict_diff(dict_a:dict,dict_b:dict)->dict:
if not isinstance(dict_a,dict):
dict_a = {}
if not isinstance(dict_b,dict):
dict_b = {}
ka = set(dict_a.keys())
kb = set(dict_b.keys())
only_a = ka - kb
only_b = kb - ka
common = ka & kb
diff_val = {}
for k in common:
va = dict_a[k]
vb = dict_b[k]
if va != vb:
diff_val[k] = {"a":va,"b":vb}
return {
"only_in_a":{k:dict_a[k] for k in only_a},
"only_in_b":{k:dict_b[k] for k in only_b},
"value_diff":diff_val
}
🎤 口述要点:用集合做差集;追问再讲嵌套递归方案。
5.5.8 带参数的重试装饰器(速记版)
import time
from functools import wraps
def retry(max_times:int,sleep_sec:float,catch_exc:tuple):
if max_times <1:
raise ValueError("max_times >=1")
if sleep_sec <0:
raise ValueError("sleep_sec >=0")
def decorator(func):
@wraps(func)
def wrapper(*args,**kwargs):
last_err = None
for attempt in range(max_times):
try:
return func(*args,**kwargs)
except catch_exc as e:
last_err = e
if attempt != max_times-1:
print(f"retry attempt {attempt+1}, err:{e}")
time.sleep(sleep_sec)
raise last_err
return wrapper
return decorator
🎤 口述要点:不捕获全部 Exception;非幂等接口禁止重试;wraps 保留函数信息。
📄 来源:003_测试开发面试全合集.md(# 第六部分:高级测开社招(8-12 年)面试考察)
6 第六部分:高级测开社招(8-12 年)面试考察
背景:工作年限 8-12 年,高级测开、测试架构师方向,北京上海大厂 / 中厂。
6.1 核心特征
- 不再考基础入门算法(两数之和、反转链表这类基本不会手撕);
- 题目偏向工程大数据处理、性能测试、平台工具、大模型评测、海量日志、并发场景;
- 算法难度:中等-中等偏上,极少 Hard DP;重点:复杂度、内存优化、流式处理、边界、并发、故障模拟;
- 很多题目是真实工作场景改编,不是纯 LeetCode 原题;
- 同时会夹杂 AI-coding / 大模型评测场景题;
- 部分公司会有 1 道中等难度 LeetCode,更多是工程编码大题。
⚠️ 注意:10 年社招,面试官看的不是"能不能写出代码",而是:你的方案选型、时间空间复杂度取舍、内存瓶颈、大数据量下怎么优化、故障、并发、可观测性。同样一道题,1-3 年测开只要求写对,10 年同学要讲清楚 trade-off。
6.2 LeetCode 真题(会出现的,中等为主)
不会考简单题,优先中等,高频:
| 题号 | 题目 | 测开场景映射 |
|---|---|---|
| LC3 | 无重复字符最长子串 | 滑动窗口,日志 / 报文处理场景 |
| LC56 | 合并区间 | 时间窗口、压测、调度任务时间区间,非常高频 |
| LC239 | 滑动窗口最大值 | 性能指标统计,QPS 窗口统计,高级测开高频 |
| LC347 | 前 K 个高频元素 | 统计 topN 接口错误、topN 慢请求,海量日志统计 |
| LC146 | LRU 缓存 | 极高频,测试平台、接口缓存、mock 服务本地缓存 |
| LC460 | LFU 缓存 | 部分大厂高级测开 / 测试架构会问,比 LRU 难度更高 |
| LC151 | 反转字符串里的单词 | 日志解析 |
| LC380 | O(1) 时间插入、删除和获取随机元素 | mock 测试场景 |
❌ 基本不考:反转链表、回文串、两数之和这类入门题。
6.3 高级测开社招:工程手写编码题(10 年社招重点,比 LeetCode 占比更高)
全部来自真实平台、自动化平台、性能平台、日志分析、mock 服务场景。
6.3.1 题 1:实现简易 LRU Cache(LC146,高频)
- 场景:测试平台做接口响应缓存、mock 接口缓存。
- 要求:put、get O(1) 时间复杂度;超过容量淘汰最久未使用数据。
- 追问:如果数据量巨大,内存放不下怎么做?多线程并发访问 LRU 会有什么问题?如何加锁?LRU 的缺点,什么场景适合换 LFU?
6.3.2 题 2:海量 Nginx 日志统计 TopK 慢请求
- 题目描述:一个超大日志文件,无法一次性全部读进内存(几十 GB),每一行日志包含接口路径、响应耗时。要求:找出响应耗时最大的前 K 个接口请求记录。
- 🎯 考察点:不能全部加载内存;堆(小顶堆);流式处理;磁盘文件分片;复杂度分析。
- 追问:如果机器多核,如何并行加速处理?
6.3.3 题 3:时间窗口 QPS 统计(滑动窗口,性能测试高频)
- 题目:实现一个工具,统计最近 60s 窗口内请求 QPS。不断收到请求时间戳;需要支持随时查询当前窗口 QPS。
- 要求:内存不能无限膨胀;过期数据自动清理。
- 追问:如果流量巨大,每秒上万请求,内存怎么优化?时间乱序到达(日志时间戳乱序)如何处理?真实日志经常发生。
6.3.4 题 4:实现一个简易 HTTP-mock 服务核心逻辑(Python)
- 需求:支持配置路由规则,匹配 path,返回预设 response;支持记录全部请求历史,后续可以查询收到的请求;支持设置规则过期时间;并发请求下,请求记录不能丢失。
- 考察:线程安全、数据结构选择、内存膨胀风险。
6.3.5 题 5:大 JSON 递归 diff(深度字典对比)
- 和之前单层 diff 不一样,10 年社招会考无限嵌套 JSON 对象 diff,支持嵌套 dict、list。
- 输出完整 diff 路径,例如 data.user.name 标记差异。
- 追问:列表如何对比?按 index 对比,还是按业务 key 匹配?两种策略的业务取舍。
6.3.6 题 6:支持指数退避的通用重试组件(升级版,区别初级版本)
- 不再简单固定 sleep;支持指数退避、最大等待上限;支持黑名单异常不重试;支持重试回调上报指标;支持取消开关。
- 追问:幂等性,哪些场景绝对不能重试;如何做熔断。
6.3.7 题 7:大文件比对工具逻辑
- 两个超大文本文件,不能全部读入内存,找出两边不相同的行。
- 考察:分块、哈希、外排序;内存和磁盘取舍。
6.4 大模型 / AI 测试方向高级测开额外考察(字节、阿里、百度、部分 AI 公司)
- 实现一个简单的评测集采样工具,对海量样本做分层抽样,保证不同类别样本分布;
- 实现 Judge 结果统计:MT-bench m2n 两两对战,统计胜率、ELO 分数;
- 写逻辑过滤 AI 输出重复、幻觉样本;
- AI-coding 高阶题:给一段复杂 AI 生成的测试平台代码,找出并发 bug、内存泄漏、线程安全问题。
6.5 10 年社招算法 / 编码面试非常重要的面试要点(加分项)
同样一道题,初级同学写对代码即可;10 年工作年限面试官重点想听下面这些: - trade-off 取舍:内存换时间 / 时间换内存;什么场景选择什么方案; - 大数据约束:当数据量很大,内存放不下怎么办,流式、分片、外排序; - 并发安全:多线程、多协程下,你的数据结构会出现什么问题,如何处理锁; - 故障与边界:超时、乱序数据、脏数据、服务崩溃; - 可观测性:埋点、日志、指标; - 主动讲清楚方案的局限性:什么场景这个方案不适用。
避坑提醒:10 年社招,如果你上来只写最简单暴力的暴力解法,不考虑大数据、内存、并发,会明显扣分。
6.6 10 年测开社招面试的现实情况总结
- 很多组不会考很多道手撕算法,一般 0-1 道算法题,更多是工程编码大题;
- 但是遇到 AI 测试、测试平台开发方向,编码和算法权重会明显拉高;
- 笔试 OA 依然会有中等 LeetCode;
- 大量场景是:给业务问题,让你设计数据结构 + 写核心片段,而不是完整大项目。
6.7 📌 备考优先级(10 年高级测开)
- 最高优先级:LRU 缓存、滑动窗口 QPS 统计、海量日志 TopK、递归 JSON diff
- LeetCode 中等高频:LC146、LC239、LC347、LC56
- 高阶工程题:mock 服务核心逻辑、指数退避重试组件、大文件比对
- AI 测试方向额外:ELO 对战统计、分层采样
📄 来源:003_测试开发面试全合集.md(# 第七部分:手撕代码全合集(完整代码 + 逐行注释))
7 第七部分:手撕代码全合集(完整代码 + 逐行注释)
每道题包含:题目描述、解题思路、完整代码、逐行注释、边界说明、面试官高频追问。
7.1 第一部分:校招 / 1-3 年社招基础
7.1.1 LC1 两数之和
题目:给定数组 nums,目标 target,返回两个下标,数组两个元素相加等于 target。没有解返回空列表。
def two_sum(nums: list[int], target: int) -> list[int]:
# 哈希表:key=数组值,value=对应下标
hashmap = {}
# 遍历,同时拿到下标与数值
for idx, num in enumerate(nums):
# 需要凑的另一半数字
need = target - num
# 如果另一半已经存在哈希表,直接返回下标
if need in hashmap:
return [hashmap[need], idx]
# 当前数字存入哈希,供后续遍历匹配
hashmap[num] = idx
# 遍历结束没有找到,返回空
return []
- 边界:nums 为空,返回 [];同一个元素不能复用。
- 复杂度:时间 O(n),空间 O(n)。
- 面试官追问:Q:暴力两层循环是什么复杂度?A:O(n²),不推荐。
7.1.2 LC206 反转链表
# 链表节点定义
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverse_list(head: ListNode) -> ListNode:
# pre保存上一个节点,初始为空
prev = None
# cur 当前遍历节点
cur = head
while cur:
# 先保存下一个节点,防止断链
nxt = cur.next
# 当前节点反向指向前面
cur.next = prev
# pre向后移动到cur
prev = cur
# cur向后移动到预先保存的下一个节点
cur = nxt
# pre是新链表头
return prev
- 边界:链表为空 / 只有一个节点。
7.1.3 LC125 验证回文串
题目:只考虑字母数字,忽略大小写,判断字符串是否是回文。
def is_palindrome(s: str) -> bool:
# 左右双指针
left, right = 0, len(s) - 1
while left < right:
# 左指针跳过非字母数字
while left < right and not s[left].isalnum():
left += 1
# 右指针跳过非字母数字
while left < right and not s[right].isalnum():
right -= 1
# 转小写对比
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
- 边界:空字符串返回 True。
7.1.4 LC56 合并区间
题目:合并重叠区间。
def merge(intervals: list[list[int]]) -> list[list[int]]:
# 输入为空直接返回
if not intervals:
return []
# 按区间起点排序
intervals.sort(key=lambda x: x[0])
# 结果数组,放入第一个区间
res = [intervals[0]]
# 从第二个区间开始遍历
for start, end in intervals[1:]:
last_start, last_end = res[-1]
# 当前区间起点<=上一个区间终点:发生重叠,合并
if start <= last_end:
res[-1][1] = max(last_end, end)
else:
# 不重叠直接追加
res.append([start, end])
return res
- 边界:空输入、区间完全包裹、区间相邻如 [1,4],[4,5]。
7.1.5 HTTP 日志解析脚本
题目:输入多行 nginx 日志,提取 url,统计每个 http 状态码次数,脏行直接跳过不崩溃。
def parse_http_log(log_text: str) -> dict:
stat_count = {}
# 空输入直接返回空统计
if not log_text:
return stat_count
# 按换行切分所有行
lines = log_text.splitlines()
for line in lines:
line = line.strip()
# 跳过空行
if not line:
continue
try:
# 找到第一个双引号位置
qs = line.find('"')
# 找第二个双引号,从qs+1位置向后搜索
qe = line.find('"', qs + 1)
# 找不到引号,格式异常,跳过本行
if qs == -1 or qe == -1:
continue
# 截取两个引号中间请求行:GET /api/xxx HTTP/1.1
req_part = line[qs + 1: qe]
req_items = req_part.split()
# 请求行格式残缺
if len(req_items) < 2:
continue
# 引号之后的文本,包含状态码
after_quote = line[qe + 1:].strip()
status_str = after_quote.split()[0]
# 判断是否是数字字符串
if not status_str.isdigit():
continue
status_code = int(status_str)
# 字典计数,get获取旧值,没有默认0
stat_count[status_code] = stat_count.get(status_code, 0) + 1
except Exception:
# 任意解析异常直接跳过该行,不中断整个脚本
continue
return stat_count
7.1.6 JSON 单层字典 diff
题目:只处理一层字典,输出 only_in_a、only_in_b、value_diff。
def json_dict_diff(dict_a: dict, dict_b: dict) -> dict:
# 防御,入参不是字典替换为空字典
if not isinstance(dict_a, dict):
dict_a = {}
if not isinstance(dict_b, dict):
dict_b = {}
# 获取全部key集合
ka = set(dict_a.keys())
kb = set(dict_b.keys())
# 集合差集
only_a_keys = ka - kb
only_b_keys = kb - ka
common_keys = ka & kb
only_in_a = {k: dict_a[k] for k in only_a_keys}
only_in_b = {k: dict_b[k] for k in only_b_keys}
value_diff = {}
for k in common_keys:
va = dict_a[k]
vb = dict_b[k]
if va != vb:
value_diff[k] = {"a": va, "b": vb}
return {
"only_in_a": only_in_a,
"only_in_b": only_in_b,
"value_diff": value_diff
}
7.1.7 带捕获异常的重试装饰器
import time
from functools import wraps
def retry(max_times: int, sleep_sec: float, catch_exc: tuple):
"""
:param max_times:最大执行总次数
:param sleep_sec:重试间隔秒
:param catch_exc:只捕获该元组内异常
"""
# 参数合法性校验
if max_times < 1:
raise ValueError("max_times必须>=1")
if sleep_sec < 0:
raise ValueError("sleep_sec不能负数")
def decorator(func):
# wraps保留原函数元信息,函数名、docstring
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
# 循环最多max_times次
for attempt in range(max_times):
try:
# 执行原函数,成功直接return
return func(*args, **kwargs)
except catch_exc as e:
last_exception = e
# 如果不是最后一轮,sleep之后重试
if attempt != max_times - 1:
print(f"[retry] attempt {attempt+1}, err: {e}")
time.sleep(sleep_sec)
# 全部重试耗尽抛出原始异常
raise last_exception
return wrapper
return decorator
7.1.8 CSV 读取过滤脏行
题目:面试手写版本,不使用标准 csv 库。过滤空行、字段数量不对的脏行。
def read_and_filter_csv(csv_text: str, expect_col_cnt: int) -> list:
result = []
if not csv_text:
return result
# 切分成多行
lines = csv_text.splitlines()
for line in lines:
line = line.strip()
# 跳过空行
if not line:
continue
# 按逗号分割
parts = line.split(",")
# 字段数量不匹配预期,丢弃该行
if len(parts) != expect_col_cnt:
continue
result.append(parts)
return result
7.2 第二部分:8-12 年高级测开 / 测试架构社招题
7.2.1 LC146 LRU 缓存实现
要点:LRU:最近最少使用淘汰;get/put O(1);超过容量淘汰最久未访问元素。Python 使用 collections.OrderedDict,底层双向链表。
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
# 设置最大容量
self.capacity = capacity
# OrderedDict:可以移动元素到末尾,弹出头部元素
self.cache = OrderedDict()
def get(self, key):
"""获取key,命中把key移动到末尾标记为最近使用;不存在返回-1"""
if key not in self.cache:
return -1
# move_to_end:把该key移到字典末尾,表示最近访问
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value) -> None:
"""写入key-value;存在则更新,不存在新增;超容量删除头部最久未使用"""
if key in self.cache:
# key已存在,更新值,移动到末尾
self.cache[key] = value
self.cache.move_to_end(key)
else:
# 新增key
self.cache[key] = value
# 判断是否超过容量
if len(self.cache) > self.capacity:
# popitem(last=False)弹出头部,最久没有访问
self.cache.popitem(last=False)
高频追问:
- 多线程并发访问会有什么问题?→ 线程不安全,需要加锁 threading.Lock。
- 如果数据太大内存放不下?→ 落盘磁盘,冷热分离。
- LRU 缺点:热点一次性大量冷数据会把热数据淘汰,可以引出 LFU。
7.2.2 海量日志 TopK 慢请求(小顶堆,内存受限场景)
思路:日志文件几十 GB,不能全部读入内存,找出响应耗时最大前 K 条记录。维护大小 K 的小顶堆。遍历每一行,堆不足 K 直接入堆;超过 K,新元素比堆顶大,弹出堆顶压入新元素。时间复杂度 O(N logK),内存只维护 K 个元素。
import heapq
def topk_slow_request(lines_iter, k: int):
"""
:param lines_iter: 迭代器,逐行读取大文件,不一次性加载全部内容
:param k: 需要top k
:return: list[(cost_ms, url)] topk耗时记录
"""
heap = []
for line in lines_iter:
line = line.strip()
if not line:
continue
# 模拟解析日志,提取耗时、url(真实场景做日志解析)
# 示例:line: "/api/query 1200" url /api/query cost 1200ms
parts = line.split()
if len(parts) < 2:
continue
try:
cost = int(parts[1])
url = parts[0]
except ValueError:
continue
# 堆大小不足k,直接压入;heapq是小顶堆
if len(heap) < k:
heapq.heappush(heap, (cost, url))
else:
# 当前记录耗时大于堆顶最小元素,替换堆顶
if cost > heap[0][0]:
heapq.heappop(heap)
heapq.heappush(heap, (cost, url))
# 堆内是topK,返回并排序,从大到小
result = sorted(heap, key=lambda x: -x[0])
return result
# -------- 使用示例:逐行读取文件,不一次性读全部 --------
# with open("big.log","r",encoding="utf-8") as f:
# res = topk_slow_request(f, k=10)
追问: - 多核机器如何加速?多进程分片读取文件,每个分片算 topK,最后合并所有分片 topK 再做一次 topK。 - 为什么不用大顶堆?大顶堆需要保存全部 N 个元素,内存爆炸。
7.2.3 滑动窗口 QPS 统计(性能测试高频)
需求:统计最近 60 秒窗口 QPS;持续收到请求时间戳;过期数据自动清理,内存不能无限膨胀。注意:真实日志时间戳可能乱序。
from collections import deque
import time
class SlidingWindowQPS:
def __init__(self, window_sec: int = 60):
self.window_sec = window_sec
# 双端队列,保存请求的unix时间戳
self.queue = deque()
def add_request(self, ts: float):
"""新增一条请求,传入时间戳(unix时间戳秒)"""
now = time.time()
expire_ts = now - self.window_sec
# 清理窗口以外过期的时间戳,队列头部过期就弹出
while self.queue and self.queue[0] < expire_ts:
self.queue.popleft()
# 加入本次请求时间戳
self.queue.append(ts)
def get_qps(self) -> float:
"""获取当前窗口QPS = 请求总数 / 窗口大小"""
now = time.time()
expire_ts = now - self.window_sec
# 查询的时候也要清理过期数据
while self.queue and self.queue[0] < expire_ts:
self.queue.popleft()
cnt = len(self.queue)
return cnt / self.window_sec
追问: - 请求量每秒上万,队列存每一个时间戳内存压力大?答:时间分片,桶统计,每个桶记录计数,不用存每一条时间戳。 - 日志时间戳乱序到达怎么处理?不能简单弹出队头,需要做时间桶容错。
7.2.4 递归深度 JSON diff(支持嵌套 dict/list)
要点:输出带路径的 diff,例如 data.user.name;区分:只在 A、只在 B、值不同。
def deep_json_diff(a, b, path: str = "$") -> list:
"""
返回diff列表,每个元素{"path":路径,"type":"only_a"/"only_b"/"diff","a_val":...,"b_val":...}
"""
diff_result = []
# 两个都是字典
if isinstance(a, dict) and isinstance(b, dict):
keys_a = set(a.keys())
keys_b = set(b.keys())
for k in keys_a - keys_b:
diff_result.append({
"path": f"{path}.{k}",
"type": "only_a",
"a_val": a[k],
"b_val": None
})
for k in keys_b - keys_a:
diff_result.append({
"path": f"{path}.{k}",
"type": "only_b",
"a_val": None,
"b_val": b[k]
})
# 共同key递归
for k in keys_a & keys_b:
sub_path = f"{path}.{k}"
sub_diff = deep_json_diff(a[k], b[k], sub_path)
diff_result.extend(sub_diff)
# 两个都是list,按索引递归对比
elif isinstance(a, list) and isinstance(b, list):
max_len = max(len(a), len(b))
for idx in range(max_len):
sub_path = f"{path}[{idx}]"
va = a[idx] if idx < len(a) else None
vb = b[idx] if idx < len(b) else None
sub_diff = deep_json_diff(va, vb, sub_path)
diff_result.extend(sub_diff)
else:
# 普通值直接对比
if a != b:
diff_result.append({
"path": path,
"type": "diff",
"a_val": a,
"b_val": b
})
return diff_result
面试官追问: - Q:列表业务场景不关心顺序,只关心集合内容怎么做? - A:不能直接按 index 对比,可以指定业务 key 把 list 转为 dict 再 diff。
7.2.5 指数退避高级重试组件
要点:区别初级版本:指数退避、最大等待上限、异常黑名单、回调钩子。
import time
from functools import wraps
def advanced_retry(max_attempt: int, base_sleep: float, max_sleep: float,
catch_exc: tuple, exclude_exc: tuple = (), callback=None):
"""
:param max_attempt:最大总尝试次数
:param base_sleep:基础休眠时间
:param max_sleep:指数退避最大休眠上限
:param catch_exc:需要捕获并重试异常
:param exclude_exc:黑名单异常,即使属于catch_exc也不重试
:param callback:重试失败回调函数,用于上报埋点
"""
if max_attempt < 1:
raise ValueError("max_attempt >=1")
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_err = None
for attempt in range(max_attempt):
try:
return func(*args, **kwargs)
except catch_exc as e:
# 黑名单异常直接抛出,不重试
if isinstance(e, exclude_exc):
raise e
last_err = e
if attempt == max_attempt - 1:
break
# 指数退避 sleep = base * (2^attempt),不超过max_sleep
sleep_time = base_sleep * (2 ** attempt)
sleep_time = min(sleep_time, max_sleep)
print(f"[advanced retry] attempt {attempt+1}, sleep {sleep_time:.2f}s err={e}")
if callback is not None:
callback(func.__name__, attempt, e)
time.sleep(sleep_time)
raise last_err
return wrapper
return decorator
追问:哪些场景不能重试?非幂等写接口(下单、支付),重试会造成脏数据。
7.2.6 简易 HTTP-Mock 核心逻辑(线程安全)
要点:只展示内存核心数据结构,不写完整 http 服务。
import threading
from dataclasses import dataclass
from typing import Optional
@dataclass
class MockRule:
path: str
resp_body: dict
expire_ts: Optional[float]
@dataclass
class RequestRecord:
path: str
body: dict
ts: float
class SimpleMockCore:
def __init__(self):
# 锁保证多线程并发安全
self.lock = threading.Lock()
# 存储mock规则 path -> MockRule
self.rules: dict[str, MockRule] = {}
# 保存收到的所有请求记录
self.request_history: list[RequestRecord] = []
def add_rule(self, rule: MockRule):
"""添加mock规则"""
with self.lock:
self.rules[rule.path] = rule
def match_rule(self, path: str, now_ts: float):
"""匹配路径,过滤过期规则"""
with self.lock:
rule = self.rules.get(path)
if rule is None:
return None
# 判断规则是否过期
if rule.expire_ts is not None and now_ts > rule.expire_ts:
del self.rules[path]
return None
return rule
def record_incoming_request(self, req_record: RequestRecord):
"""记录收到请求,线程安全"""
with self.lock:
self.request_history.append(req_record)
追问:大量请求,request_history 无限膨胀怎么办?做最大长度限制,或者落盘。
📄 来源:003_测试开发面试全合集.md(# 附录 A:AI-Coding 面试题(无代码,面试口述))
8 附录 A:AI-Coding 面试题(无代码,面试口述)
- 题目 1:Review AI 生成有 bug 的接口自动化代码,找出缺陷。
- 题目 2:写 prompt 指挥 AI 完成接口自动化,AI 输出之后做哪些校验。
- 题目 3:AI 写的脚本跑不通你的排查步骤。
参考口述模板见本文档第四部分,面试直接口述即可。
📄 来源:003_测试开发面试全合集.md(# 附录 B:面试编码通用答题模板)
9 附录 B:面试编码通用答题模板
- 拿到题目先口述思路,再写代码。
- 主动口述你考虑的边界 case。
- 写完代码,口头模拟跑一遍样例输入输出。
- 工程类题目重点强调:异常捕获、容错。
- 面试官追问,再扩展进阶方案(递归 diff、指数退避、分片处理大文件)。
- 高级岗位一定要主动讲:trade-off,时间换内存 / 内存换时间,并发安全,局限性。
测开面试通用小套路(背下来): - 拿到题目,先口述思路,不要上来直接写代码; - 主动说出你考虑到的边界 case(空输入、异常输入、极端值),这是测开面试官非常看重的点; - 写完代码,自己口头说测试用例,模拟跑一遍样例; - 如果是工程脚本题,一定要强调异常捕获、容错,这是和普通后端算法面试最大区别; - 面试官追问再扩展进阶方案(嵌套 diff、指数退避重试等)。
10 开发工具与工程生态(TOML/YAML/Poetry/Textual/Qodo)
来源:WorkBuddy《面试知识点详解》主题 J21/J20/H11/H14(豆包对话《Poetry 虚拟环境管理 - 豆包》《Agent 编排:原理 + 核心模式 + 完整实践》《MCP 原理及大模型测试应用实践》) 整理入库:2026-09-14(WorkBuddy 137 条知识点覆盖检查补录) 定位:工程工具类面试速查——配置格式选型(TOML/YAML)、Python 工程化(Poetry)、终端 TUI(Textual)、AI 代码测试工具(Qodo)。
10.1 TOML 与 YAML 的区别(J21)
WorkBuddy 文档原文(J21):
两者都是人类可读的配置文件格式,但设计理念相反。 选型:简单/无歧义配置选 TOML;复杂嵌套层级选 [YAML]。
★ 入库补充(对比表):
| 维度 | TOML | YAML |
|---|---|---|
| 设计理念 | 明确、无歧义(类型原生:表/数组/日期/布尔) | 表达力强、人类可读(缩进敏感 + 锚点别名/多文档) |
| 类型系统 | 原生类型(无引号歧义),无需引号 | 字符串/数字靠解析器推断(Norway 问题:no 被解析为 false) |
| 嵌套 | 方括号表 [a.b] 显式分层 |
缩进分层(tab 缩进直接报错) |
| 生态 | Python 标配(pyproject.toml)、Rust Cargo | CI/CD(GitHub Actions)、K8s、Docker Compose、Ansible |
| 典型坑 | 无(歧义少) | 锚点误用、缩进不一致、on/off/no 被强转布尔 |
| 一句话选型 | 简单/无歧义/机器生成的配置 | 复杂嵌套层级/人类手写维护的配置 |
10.2 Poetry 使用与虚拟环境管理(J20)
WorkBuddy 文档原文(J20):
Poetry 是集依赖管理 + 虚拟环境 + 打包发布于一体的 Python 工具,替代
pip + virtualenv + setup.py的痛点组合。
★ 入库补充(核心流程与命令):
- 核心闭环:
pyproject.toml声明依赖 →poetry add/remove管理依赖 →poetry lock锁定精确版本(lockfile 可复现)→poetry install自动建虚拟环境并安装 →poetry build/publish打包发布。 - 面试要点:①
poetry.lock保证团队/CI 环境依赖一致(可复现构建);②自动创建虚拟环境,无需手动 activate(poetry run或poetry shell);③与 pip 区别:pip 无锁文件无依赖解析,Poetry 有解析器+锁文件+发布一体化。
10.3 Textual 终端可视化面板(TUI 实战)(H11)
WorkBuddy 文档原文(H11):
用 Textual 框架做 Agent 编排监控面板:任务列表、实时日志、状态颜色(运行中/成功/失败)、按钮交互、自动刷新;纯终端运行,服务器/SSH 可用。 Textual 最核心 4 个知识点:①
compose()搭界面结构 ②reactive响应式状态(数据变界面自动刷新)③on_button_pressed按钮事件 ④[事件驱动装饰器@on]
★ 入库补充(4 个核心点展开 + 与 CUI/GUI 定位):
- ①
compose():声明式定义界面结构(Widget 树),类似 React 的 render。 - ②
reactive:响应式状态变量,数据一变界面自动重绘(省掉手动刷新)。 - ③
on_button_pressed/@on(Button.Pressed):事件驱动交互(按钮点击、定时器刷新)。 - ④ 定位:TUI = 字符排版"伪图形界面"(非 GUI),适合服务器/SSH 场景的运维与 Agent 监控控制台;与 CUI(纯命令行)、GUI(网页后台/LangGraph Studio)三类界面形态(见 002 文档 10 章 CUI/TUI/GUI 区别)。
10.4 Qodo:AI 代码测试工具(H14)
WorkBuddy 文档原文(H14):
Qodo(前身 CodiumAI),以色列 2022 成立,2024-07 改名,主打 AI 代码质量+自动化测试+代码审查,对标 Copilot 但侧重"测代码/审代码/保质量"。
★ 入库补充(面试展开):
- 能力三角:AI 代码质量(分析/建议)、自动化测试(基于代码行为生成测试)、代码审查(PR 审查助手)。
- 与 Copilot 的差异定位:Copilot 偏"写代码"(生成补全),Qodo 偏"测代码/审代码/保质量"(质量守护),是 AI 编程工具在测试侧的典型代表。
- 面试价值:讲"AI 提效测试"时可作为工具案例(类似 002 第 4 章的 5 个 Skill、Playwright 等),证明关注业界工具生态。