AI 面试知识体系

003:Coding与AI编程

Coding 能力与 AI-Coding 专题(手撕算法/测开脚本题/Review AI代码/Prompt指挥)

📑 目录

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 容易遇见

✅ 测开避坑:Hard 题几乎不考;DP、拓扑排序、复杂图题不用花大量时间。

2.2 测开特有手写脚本题(不是 LeetCode,面试白板手写)

这一类比纯算法更常考,是测开特色:

  1. 手写函数:解析一段 http 日志字符串,提取出全部请求 url、状态码,统计每个状态码出现次数
  2. 手写函数:对比两个 json 字典,输出 key 不一样、value 不一致的 diff 差异集合
  3. 手写简易重试装饰器:对接口请求函数,失败自动重试 N 次,超时抛出异常
  4. 手写读取 csv,过滤异常行,输出过滤后的结果

2.3 刷题优先级(测开)



📄 来源: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:真实工作场景问答(口头)

3.5 面试实操建议

3.6 备考优先级排序

  1. 先搞定:字符串、哈希、数组双指针手撕算法 + 测开手写脚本题;
  2. 再练习:Review 有 bug 的 AI 代码、自己写测开场景 prompt;
  3. 口头能讲清楚 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 💡 面试小提示



📄 来源: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 核心特征

⚠️ 注意: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,高频)

6.3.2 题 2:海量 Nginx 日志统计 TopK 慢请求

6.3.3 题 3:时间窗口 QPS 统计(滑动窗口,性能测试高频)

6.3.4 题 4:实现一个简易 HTTP-mock 服务核心逻辑(Python)

6.3.5 题 5:大 JSON 递归 diff(深度字典对比)

6.3.6 题 6:支持指数退避的通用重试组件(升级版,区别初级版本)

6.3.7 题 7:大文件比对工具逻辑

6.4 大模型 / AI 测试方向高级测开额外考察(字节、阿里、百度、部分 AI 公司)

6.5 10 年社招算法 / 编码面试非常重要的面试要点(加分项)

同样一道题,初级同学写对代码即可;10 年工作年限面试官重点想听下面这些: - trade-off 取舍:内存换时间 / 时间换内存;什么场景选择什么方案; - 大数据约束:当数据量很大,内存放不下怎么办,流式、分片、外排序; - 并发安全:多线程、多协程下,你的数据结构会出现什么问题,如何处理锁; - 故障与边界:超时、乱序数据、脏数据、服务崩溃; - 可观测性:埋点、日志、指标; - 主动讲清楚方案的局限性:什么场景这个方案不适用。

避坑提醒:10 年社招,如果你上来只写最简单暴力的暴力解法,不考虑大数据、内存、并发,会明显扣分。

6.6 10 年测开社招面试的现实情况总结

6.7 📌 备考优先级(10 年高级测开)



📄 来源: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 []

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

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

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 面试题(无代码,面试口述)

参考口述模板见本文档第四部分,面试直接口述即可。



📄 来源:003_测试开发面试全合集.md(# 附录 B:面试编码通用答题模板)

9 附录 B:面试编码通用答题模板

  1. 拿到题目先口述思路,再写代码。
  2. 主动口述你考虑的边界 case。
  3. 写完代码,口头模拟跑一遍样例输入输出。
  4. 工程类题目重点强调:异常捕获、容错。
  5. 面试官追问,再扩展进阶方案(递归 diff、指数退避、分片处理大文件)。
  6. 高级岗位一定要主动讲: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 的痛点组合。

★ 入库补充(核心流程与命令):

10.3 Textual 终端可视化面板(TUI 实战)(H11)

WorkBuddy 文档原文(H11):

用 Textual 框架做 Agent 编排监控面板:任务列表、实时日志、状态颜色(运行中/成功/失败)、按钮交互、自动刷新;纯终端运行,服务器/SSH 可用。 Textual 最核心 4 个知识点:①compose() 搭界面结构 ②reactive 响应式状态(数据变界面自动刷新)③on_button_pressed 按钮事件 ④[事件驱动装饰器 @on]

★ 入库补充(4 个核心点展开 + 与 CUI/GUI 定位):

10.4 Qodo:AI 代码测试工具(H14)

WorkBuddy 文档原文(H14):

Qodo(前身 CodiumAI),以色列 2022 成立,2024-07 改名,主打 AI 代码质量+自动化测试+代码审查,对标 Copilot 但侧重"测代码/审代码/保质量"。

★ 入库补充(面试展开):