AI 用多了,别忘了 Python:测试工程师必会的 10 组模块

0 阅读8分钟

图片

「漫说测试」· 基本功

这两年大家都在聊 prompt、聊 Agent、聊评测。 但真到了项目里,把你和"只会点点点"区分开的,往往还是那几行 Python。

图片

【贴图1】


一、路径与文件:pathlib / shutil / tempfile

为什么测试需要:测试脚本一半的时间在处理文件——读用例、存报告、准备上传文件、清理临时目录。

pathlib:别再拼字符串了

图片

from pathlib import Path

cases = Path("tests/cases")
for f in cases.rglob("*.json"):        # 递归找
    data = f.read_text(encoding="utf-8")

out = Path("reports") / "run.json"     # 用 / 拼路径
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text("{}", encoding="utf-8")

坑:Path("a") / "b" 在 Windows 上也正确;但把路径传给外部命令时,记得 str(p)。

shutil:复制、删除、找可执行文件

图片

import shutil

shutil.copytree("fixtures", "tmp/fixtures", dirs_exist_ok=True)
shutil.rmtree("tmp", ignore_errors=True)
print(shutil.disk_usage("/").free)      # 磁盘够不够
print(shutil.which("chromedriver"))     # 环境到底装没装

坑:rmtree 是真删。测试里删目录前,先断言路径在 tmp 下。

tempfile:让每个用例有自己的沙箱

图片

import tempfile
from pathlib import Path

def test_upload():
    with tempfile.TemporaryDirectory() as d:
        p = Path(d) / "a.txt"
        p.write_text("hello", encoding="utf-8")
        assert p.read_text() == "hello"
    # 退出 with 自动清理

坑:NamedTemporaryFile 在 Windows 上默认不能再次打开,测试文件建议用 TemporaryDirectory + 普通文件。

图片

【贴图2】


二、数据格式:json / csv / yaml / configparser

为什么测试需要:用例、配置、报告、Mock 数据,全是格式转换。

json

图片

import json

data = json.loads(resp.text)                       # 解析
print(json.dumps(data, ensure_ascii=False, indent=2))  # 中文不转义

坑:json.loads 遇到空字符串会抛 JSONDecodeError。接口返回 204 时先判空。

csv

图片

import csv

with open("cases.csv", newline="", encoding="utf-8-sig") as f:
    for row in csv.DictReader(f):
        print(row["case_id"], row["expected"])

坑:不加 newline="",Windows 上会多出空行;utf-8-sig 是为了让 Excel 打开不乱码。

yaml

图片

import yaml

with open("config.yaml", encoding="utf-8") as f:
    cfg = yaml.safe_load(f)      # 只用 safe_load

坑:yaml.load 可以执行任意对象构造,永远别用于不可信输入。

configparser

图片

import configparser

cfg = configparser.ConfigParser()
cfg.read("pytest.ini", encoding="utf-8")
print(cfg.get("pytest", "addopts", fallback=""))

坑:默认会把 % 当插值符。密码里有 % 时,用 ConfigParser(interpolation=None)。

图片

【贴图3】


三、接口与网络:requests / httpx / urllib

为什么测试需要:接口测试的主战场。

requests:够用,但别裸用

图片

import requests

s = requests.Session()                 # 复用连接、共享 cookie
r = s.post(
    f"{BASE}/api/login",
    json={"username": "alice", "password": "Passw0rd!"},
    timeout=(3, 10),                   # 连接 3s,读取 10s
)
r.raise_for_status()
token = r.json()["token"]

三个必设:

  • timeout

    :不设会挂死;

  • Session

    :不然每次重新握手;

  • raise_for_status()

    :不然 500 也会被当成"成功返回"。

httpx:需要异步或 HTTP/2 时用它

图片

import httpx

with httpx.Client(http2=True, timeout=10) as c:
    r = c.get(f"{BASE}/api/orders")

异步场景:

async with httpx.AsyncClient() as c:
    rs = await asyncio.gather(*[c.get(url) for url in urls])

urllib:不装第三方库时的兜底

图片

from urllib.parse import urlencode, quote

q = urlencode({"q": "测试 用例", "page": 1})   # q=%E6%B5%8B%E8%AF%95...

坑:urlencode 默认用 quote_plus,空格变 +;签名场景要用 quote,别搞混。

图片

【贴图4】


四、并发:concurrent.futures / threading / asyncio

为什么测试需要:并发下单、批量造数、多环境并行验证。

ThreadPoolExecutor:IO 密集首选

图片

from concurrent.futures import ThreadPoolExecutor, as_completed

def check(env):
    return env, requests.get(f"{env}/health", timeout=5).status_code

with ThreadPoolExecutor(max_workers=8) as pool:
    futures = [pool.submit(check, e) for e in ENVS]
    for f in as_completed(futures):
        print(f.result())

坑:max_workers 不是越大越好;打自己的测试环境,8–16 足够,太大反而触发限流。

threading:需要共享状态时

图片

import threading

lock = threading.Lock()
counter = 0

def inc():
    global counter
    with lock:
        counter += 1

坑:Python 有 GIL,但 += 仍不是原子操作。别用"没加锁也没出错"来证明线程安全。

asyncio:高并发接口压测/巡检

图片

import asyncio, httpx

async def one(c, url):
    r = await c.get(url)
    return r.status_code

async def main(urls):
    async with httpx.AsyncClient(timeout=10) as c:
        sem = asyncio.Semaphore(50)          # 控制并发
        async def guarded(u):
            async with sem:
                return await one(c, u)
        return await asyncio.gather(*[guarded(u) for u in urls])

坑:asyncio.gather 默认一个异常会向上抛,其余任务继续跑但不返回结果。需要 return_exceptions=True。

图片

【贴图5】


五、测试框架与替身:pytest / mock / faker / hypothesis

为什么测试需要:这是"测试工程师"和"会写 Python 的人"的分水岭。

pytest:fixture 是核心

图片

import pytest

@pytest.fixture(scope="session")
def api():
    s = requests.Session()
    s.headers["Authorization"] = f"Bearer {login()}"
    yield s
    s.close()

@pytest.mark.parametrize("status,ok", [(200, True), (401, False)])
def test_auth(api, status, ok):
    r = api.get("/api/me")
    assert (r.status_code == 200) is ok

坑:fixture 的 scope 只能"小依赖大",反过来报 ScopeMismatch。

unittest.mock:把外部依赖换掉

图片

from unittest.mock import patch, MagicMock

@patch("app.pay.client")
def test_pay_fail(mock_client):
    mock_client.charge.side_effect = TimeoutError
    with pytest.raises(TimeoutError):
        pay(100)
    mock_client.charge.assert_called_once_with(amount=100)

坑:patch 的路径要写"被使用的地方",不是"定义的地方"。

faker:造人话数据

图片

from faker import Faker

fake = Faker("zh_CN")
fake.name(), fake.phone_number(), fake.company()
fake.unique.email()          # 保证唯一

坑:unique 在大量造数时会耗尽,记得捕获 UniquenessException。

hypothesis:让机器帮你找边界

图片

from hypothesis import given, strategies as st

@given(st.integers(min_value=0, max_value=10**6))
def test_amount(amount):
    assert calc(amount) >= 0

坑:它会保存失败样本到 .hypothesis/,别把目录提交进仓库(加进 .gitignore)。

图片

【贴图6】


六、数据库:sqlite3 / pymysql / redis

为什么测试需要:校验落库、准备数据、清理现场。

sqlite3:本地测试最省事

图片

import sqlite3

conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
cur = conn.execute("select * from orders where id = ?", (1,))
row = cur.fetchone()
print(row["status"])

坑:必须用 ? 占位符。字符串拼接就是 SQL 注入,测试代码也一样。

pymysql:MySQL 校验

图片

import pymysql

conn = pymysql.connect(host="127.0.0.1", user="qa", password="***",
                       database="shop", cursorclass=pymysql.cursors.DictCursor)
with conn.cursor() as cur:
    cur.execute("select status from orders where id=%s", (1,))
    print(cur.fetchone())

坑:pymysql 默认不开自动提交,写操作后要 conn.commit(),否则"查不到刚写的数据"。

redis:缓存校验与清理

图片

import redis

r = redis.Redis(decode_responses=True)
r.set("k", "v", ex=60)
print(r.ttl("k"))

坑:测试环境务必用独立 db(db=1),别把开发缓存清了。

图片

【贴图7】


七、系统与进程:subprocess / psutil / signal

为什么测试需要:起服务、杀进程、查内存、做超时。

subprocess:起服务与执行命令

图片

import subprocess

p = subprocess.run(
    ["python", "app.py"],
    capture_output=True, text=True, timeout=30, check=False,
)
print(p.returncode, p.stdout, p.stderr)

坑:shell=True 有注入风险;参数用列表传,别拼字符串。

psutil:进程与资源

图片

import psutil

proc = psutil.Process(pid)
print(proc.cpu_percent(interval=1), proc.memory_info().rss / 1024 / 1024, "MB")
print(len(proc.net_connections()))     # 连接泄漏排查

坑:cpu_percent() 第一次调用返回 0,需要传 interval 或调两次。

signal:给用例加超时(仅 Unix)

图片

import signal

def handler(signum, frame):
    raise TimeoutError("case timeout")

signal.signal(signal.SIGALRM, handler)
signal.alarm(10)
try:
    run_case()
finally:
    signal.alarm(0)

坑:Windows 没有 SIGALRM。跨平台用 func_timeout 或框架自带超时。

图片

【贴图8】


八、日志与排错:logging / traceback / pdb / faulthandler

为什么测试需要:失败时能不能自证,决定了别人信不信你的测试。

logging:别用 print

图片

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
log = logging.getLogger("api")
log.info("request", extra={"url": url, "status": r.status_code})

坑:basicConfig 只在第一次调用生效;子进程/插件里重复配置会"没反应"。

traceback:把异常变成可读文本

图片

import traceback

try:
    run()
except Exception:
    log.error("case failed:\n%s", traceback.format_exc())

坑:format_exc() 只能取"当前"异常;存日志时别只存 str(e),会丢掉堆栈。

pdb:本地调试

图片

breakpoint()        # 3.7+,等价于 pdb.set_trace()

常用:n 下一行、s 进入函数、p x 打印、l 看上下文、c 继续、q 退出。

坑:CI 里别留 breakpoint(),会直接卡住。

faulthandler:卡死时打出堆栈

图片

import faulthandler

faulthandler.enable()
faulthandler.dump_traceback_later(60, exit=True)   # 60s 后打印并退出

坑:它是"兜底诊断",不是精确超时控制,CI 作业级超时仍要保留。

图片

【贴图9】


九、时间与编码:datetime / hashlib / base64 / secrets

为什么测试需要:时间断言、签名、加密参数,是接口测试的高频坑。

datetime + zoneinfo:别再用本地时间

图片

from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo

now = datetime.now(timezone.utc)
sh = now.astimezone(ZoneInfo("Asia/Shanghai"))
print((sh + timedelta(days=1)).isoformat())

坑:datetime.now() 不带时区,跨时区比较必错。统一用 UTC 存储,展示再转。

hashlib + hmac:签名校验

图片

import hashlib, hmac

sig = hmac.new(b"secret", body, hashlib.sha256).hexdigest()
print(hmac.compare_digest(sig, expected))   # 防时序攻击

坑:比较签名用 compare_digest,不要用 ==。

base64:传输编码,不是加密

图片

import base64

b64 = base64.b64encode(b"hello").decode()
raw = base64.b64decode(b64)

坑:URL 安全场景用 urlsafe_b64encode,否则 +``/ 会被转义。

secrets:生成测试凭证

图片

import secrets

token = secrets.token_hex(16)

坑:别用 random 生成 token——它可预测,测试数据也别用。

图片

【贴图10】


十、性能与质量:timeit / cProfile / coverage

为什么测试需要:你得能回答"慢在哪"和"测了多少"。

timeit:比较两种写法

图片

import timeit

t1 = timeit.timeit(lambda: json.dumps(data), number=1000)
t2 = timeit.timeit(lambda: orjson.dumps(data), number=1000)

坑:单次跑噪声大,用 repeat 取最小值。

cProfile:找热点函数

图片

import cProfile, pstats

cProfile.runctx("run_suite()", globals(), locals(), "prof.out")
pstats.Stats("prof.out").sort_stats("cumtime").print_stats(20)

坑:cProfile 会放大函数调用开销,适合找"谁占了大头",不适合精确计时。

coverage:覆盖率

图片

coverage run -m pytest
coverage report -m
coverage xml -o coverage.xml

坑:覆盖率只看"执行过",不看"断言有效"。100% 覆盖也可能一个 bug 都没抓到。


十一、如果只学五组,学这五组

时间有限的话,按这个顺序:

  1. pathlib + json

    :所有脚本的地基;

  2. requests.Session + timeout

    :接口测试的底线;

  3. pytest fixture + parametrize

    :从"写脚本"到"搭框架";

  4. unittest.mock

    :能把外部依赖切开;

  5. logging + traceback

    :失败时能自证。

这五组吃透,剩下的都是查文档的事。

图片

【贴图11】


十二、面试大礼包

光看不够,面试是要说出来的。

我把这 10 组模块整理成了一份**《测试工程师 Python 必会模块 + 面试 50 问》**:

  • 模块速查卡

    :每组 3–5 个核心 API + 一个测试场景;

  • 50 道面试题

    :按模块分组,每题带参考答案和追问;

  • 20 个高频坑

    :都是"看起来会、一写就错"的那种;

  • 一份学习顺序表

    :四周怎么排。

**公众号后台回复「pypack」,领取《测试工程师 Python 必会模块 + 面试 50 问》。**👉

图片

【贴图12】


最后

AI 能帮你写代码,但它不知道你的环境、你的约束、你的历史包袱。

这些东西,最后还是落在你对语言和系统的理解上。

工具决定你跑多快,基本功决定你能跑多远。