当前位置:首页>python>Python 接口自动化:10 个装饰器,从懵逼到上手

Python 接口自动化:10 个装饰器,从懵逼到上手

  • 2026-10-11 08:38:03
Python 接口自动化:10 个装饰器,从懵逼到上手

写接口自动化测试,光会用 requests 发请求是不够的。真实项目里你会遇到各种烦心事:接口偶尔超时要重试、测试前要先登录获取 token、失败了想看详细日志、测试用例跑太慢想知道哪里卡住了……

这些事情如果每个测试函数都去写一遍,代码会乱成一锅粥。装饰器就是专门解决这类"每次都要做但不想重复写"的问题的。

先说清楚装饰器是什么,再看具体的。‍

装饰器是个什么东西

举个最简单的例子:

def say_hello(func):    def wrapper(*args, **kwargs):        print("开始执行")        result = func(*args, **kwargs)        print("执行完毕")        return result    return wrapper@say_hellodef add(a, b):    return a + badd(1, 2)

# 输出:

# 开始执行

# 执行完毕

@say_hello 就是在说:"每次调用 add 之前,先套一层 say_hello 的逻辑"。你可以把装饰器理解成一个"包装纸",把原来的函数包起来,前后加点料。

明白了这个,下面的 10 个例子就好懂了。‍

1. 失败自动重试 @retry

场景:你测试某个下单接口,偶尔因为网络抖动返回 500,但再请求一次就好了。这种情况不能判定为测试失败,需要重试几次再说。

import timeimport functoolsdef retry(max_times=3, interval=1):    """    max_times: 最多重试几次    interval:  每次重试间隔几秒    """    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            for attempt in range(1, max_times + 1):                try:                    return func(*args, **kwargs)                except Exception as e:                    print(f"第 {attempt} 次执行失败:{e}")                    if attempt < max_times:                        time.sleep(interval)                    else:                        raise  # 重试完了还是失败,就把错误抛出去        return wrapper    return decorator# ---- 使用示例 ----import requests@retry(max_times=3, interval=2)def test_order_api():    resp = requests.post("http://example.com/api/order", json={"item_id": 1})    assert resp.status_code == 200, f"接口返回异常:{resp.status_code}"test_order_api()

加上 @retry(max_times=3, interval=2) 之后,失败了会自动等 2 秒重试,最多试 3 次。比在每个测试函数里手写 for 循环干净多了。

2. 接口耗时统计 @timeit

场景:产品说登录接口很慢,你想知道到底慢多少,以及哪个接口最耗时。

import timeimport functoolsdef timeit(func):    @functools.wraps(func)    def wrapper(*args, **kwargs):        start = time.perf_counter()        result = func(*args, **kwargs)        end = time.perf_counter()        print(f"[耗时] {func.__name__} 执行了 {end - start:.3f} 秒")        return result    return wrapper# ---- 使用示例 ----@timeitdef test_login_api():    resp = requests.post("http://example.com/api/login", json={        "username": "test_user",        "password": "123456"    })    assert resp.status_code == 200

test_login_api()

# 输出: [耗时] test_login_api 执行了 0.213 秒

批量跑测试时,把每个用例的耗时打出来,性能问题一眼就看出来了。

3. 自动登录获取 Token @with_token

场景:大部分接口需要在请求头里带 Authorization: Bearer xxx。每个测试用例都写一遍登录逻辑太蠢,而且 token 过期了还要改好几个地方。

import functoolsimport requests# 模拟一个全局 token 缓存_token_cache = {}def get_token():    """统一登录,获取 token"""    if "token" not in _token_cache:        resp = requests.post("http://example.com/api/login", json={            "username": "auto_test",            "password": "test123"        })        _token_cache["token"] = resp.json()["token"]    return _token_cache["token"]def with_token(func):    @functools.wraps(func)    def wrapper(*args, **kwargs):        token = get_token()        # 把 token 通过 kwargs 传给测试函数        kwargs["token"] = token        return func(*args, **kwargs)    return wrapper# ---- 使用示例 ----@with_tokendef test_get_user_info(token):    headers = {"Authorization": f"Bearer {token}"}    resp = requests.get("http://example.com/api/user/info", headers=headers)    assert resp.status_code == 200    assert resp.json()["username"] == "auto_test"test_get_user_info()

登录逻辑只在 get_token() 里写一次,token 还做了缓存,多个用例复用同一个 token,不会反复登录。

4. 失败时打印详细日志 @log_on_failure

场景:用例跑失败了,只看到 AssertionError 啥都不知道。你想自动把请求和响应的详情打出来,方便排查。

import functoolsimport tracebackdef log_on_failure(func):    @functools.wraps(func)    def wrapper(*args, **kwargs):        try:            return func(*args, **kwargs)        except Exception as e:            print(f"\n{'='*50}")            print(f"用例失败:{func.__name__}")            print(f"错误信息:{e}")            print(f"完整堆栈:\n{traceback.format_exc()}")            print(f"{'='*50}\n")            raise  # 依然抛出异常,让测试框架记录失败    return wrapper# ---- 使用示例 ----@log_on_failuredef test_create_user():    resp = requests.post("http://example.com/api/user", json={        "name": "张三",        "email": "invalid-email"  # 故意传个非法邮箱触发失败    })    assert resp.status_code == 201, f"实际响应:{resp.status_code}, body: {resp.text}"test_create_user()

失败时会打出完整堆栈和自定义的错误信息,不用再去猜为什么挂了。

5. 跳过指定环境的用例 @skip_in_env

场景:有些破坏性测试(比如删除数据)只在测试环境跑,绝对不能在生产环境跑。加个环境判断装饰器,彻底消灭人工误操作。

import osimport functoolsdef skip_in_env(*envs):    """    如果当前环境在 envs 列表里,就跳过这个测试    用法: @skip_in_env("prod", "staging")    """    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            current_env = os.getenv("TEST_ENV", "dev")            if current_env in envs:                print(f"[跳过] {func.__name__} 在 {current_env} 环境下不执行")                return None            return func(*args, **kwargs)        return wrapper    return decorator# ---- 使用示例 ----@skip_in_env("prod", "staging")  # 生产和预发环境跳过def test_delete_all_test_data():    resp = requests.delete("http://example.com/api/testdata/clear")    assert resp.status_code == 200# 设置环境变量os.environ["TEST_ENV"] = "prod"test_delete_all_test_data()# 输出: [跳过] test_delete_all_test_data 在 prod 环境下不执行

6. 接口响应结构校验 @validate_schema

场景:接口返回了 200,但返回的 JSON 结构不对,比如少了某个字段或者字段类型变了。这种情况单纯判断状态码是发现不了的。

import functoolsdef validate_schema(expected_keys):    """    检查响应 JSON 里是否包含所有必需字段    expected_keys: 必须存在的字段列表,如 ["id", "name", "email"]    """    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            resp = func(*args, **kwargs)            if resp is None:                return resp            body = resp.json()            missing = [k for k in expected_keys if k not in body]            if missing:                raise AssertionError(f"响应缺少字段:{missing},实际返回:{list(body.keys())}")            print(f"[校验通过] {func.__name__} 响应结构正常")            return resp        return wrapper    return decorator# ---- 使用示例 ----@validate_schema(expected_keys=["id", "username", "email", "created_at"])def test_get_user_detail():    resp = requests.get("http://example.com/api/user/1")    assert resp.status_code == 200    return resp  # 注意:装饰器需要函数返回 resptest_get_user_detail()

字段列表在装饰器里统一维护,接口文档更新了只改一处。

7. 限制用例最长执行时间 @timeout

场景:某个接口偶发性死循环或服务器假死,你的测试用例会一直卡着跑。加个超时装饰器,超过指定时间就强制报错。

import functoolsimport threadingdef timeout(seconds=10):    """    超过 seconds 秒没执行完,抛出 TimeoutError    """    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            result = [None]            exception = [None]            def target():                try:                    result[0] = func(*args, **kwargs)                except Exception as e:                    exception[0] = e            thread = threading.Thread(target=target)            thread.daemon = True            thread.start()            thread.join(timeout=seconds)            if thread.is_alive():                raise TimeoutError(f"{func.__name__} 执行超时(>{seconds}秒)")            if exception[0]:                raise exception[0]            return result[0]        return wrapper    return decorator# ---- 使用示例 ----@timeout(seconds=5)def test_slow_api():    # 模拟一个需要 8 秒才响应的接口    import time    time.sleep(8)    resp = requests.get("http://example.com/api/report/generate")    assert resp.status_code == 200test_slow_api()# 输出: TimeoutError: test_slow_api 执行超时(>5秒)

8. 测试数据自动清理 @cleanup

场景:测试创建用户的接口,每次跑完要把创建的测试数据删掉,不然数据库会越来越脏,后续测试也会受影响。

import functoolsdef cleanup(cleanup_func):    """    测试执行完后(不管成功还是失败),自动调用 cleanup_func 清理数据    """    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            created_ids = []            try:                result = func(*args, created_ids=created_ids, **kwargs)                return result            finally:                if created_ids:                    cleanup_func(created_ids)        return wrapper    return decoratordef delete_test_users(user_ids):    print(f"清理测试用户:{user_ids}")    for uid in user_ids:        requests.delete(f"http://example.com/api/user/{uid}")# ---- 使用示例 ----@cleanup(cleanup_func=delete_test_users)def test_create_multiple_users(created_ids):    for i in range(3):        resp = requests.post("http://example.com/api/user", json={"name": f"测试用户{i}"})        assert resp.status_code == 201        created_ids.append(resp.json()["id"])  # 把创建的 id 存进去,cleanup 会拿去删test_create_multiple_users()# 用例跑完后自动输出: 清理测试用户:[101, 102, 103]

finally 保证了不管测试成功还是失败,清理逻辑一定会执行。

9. 请求和响应全量记录 @record_http

场景:测试报告里需要把每个用例的请求/响应详情记录下来,方便出问题时复盘,也方便给开发看证据。

import functoolsimport jsonfrom unittest.mock import patch# 简单模拟一个日志收集器http_records = []def record_http(func):    @functools.wraps(func)    def wrapper(*args, **kwargs):        original_request = requests.Session.request        def patched_request(self, method, url, **req_kwargs):            resp = original_request(self, method, url, **req_kwargs)            record = {                "test_case": func.__name__,                "method": method.upper(),                "url": url,                "request_body": req_kwargs.get("json") or req_kwargs.get("data"),                "status_code": resp.status_code,                "response_body": resp.text[:500]  # 只记前 500 个字符            }            http_records.append(record)            print(f"[HTTP记录] {method.upper()}{url} -> {resp.status_code}")            return resp        with patch.object(requests.Session, "request", patched_request):            return func(*args, **kwargs)    return wrapper# ---- 使用示例 ----@record_httpdef test_user_workflow():    # 登录    requests.post("http://example.com/api/login", json={"username": "u1", "password": "p1"})    # 查用户信息    requests.get("http://example.com/api/user/info")test_user_workflow()print("\n完整 HTTP 记录:")print(json.dumps(http_records, ensure_ascii=False, indent=2))

用 unittest.mock.patch 拦截了所有 HTTP 请求,不需要改任何发请求的代码。

10. 并发压测快捷装饰器 @concurrent

场景:你想快速验证某个接口在 10 个并发请求下会不会出错,不想专门装 locust,就想临时跑一下。

import functoolsimport threadingdef concurrent(users=10):    """    同时用 users 个线程并发执行这个测试函数    任何一个线程失败,整体标记为失败    """    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            errors = []            lock = threading.Lock()            def run():                try:                    func(*args, **kwargs)                except Exception as e:                    with lock:                        errors.append(str(e))            threads = [threading.Thread(target=run) for _ in range(users)]            for t in threads:                t.start()            for t in threads:                t.join()            if errors:                raise AssertionError(                    f"{len(errors)}/{users} 个并发请求失败:\n" + "\n".join(errors[:5])                )            print(f"[并发测试] {users} 个并发全部通过")        return wrapper    return decorator# ---- 使用示例 ----@concurrent(users=10)def test_search_api_concurrent():    resp = requests.get("http://example.com/api/search", params={"q": "python"})    assert resp.status_code == 200    assert "results" in resp.json()test_search_api_concurrent()# 输出: [并发测试] 10 个并发全部通过

10 个线程同时打这个接口,任何一个失败都会报告出来,顺带告诉你失败的比例。

装饰器可以叠加

上面这些装饰器可以随意组合。比如一个接口测试,你想同时做:重试、计时、失败打日志:

@timeit@retry(max_times=3, interval=1)@log_on_failuredef test_payment_api():    resp = requests.post("http://example.com/api/pay", json={"order_id": "123"})    assert resp.status_code == 200test_payment_api()

装饰器从下往上执行:先套 log_on_failure,再套 retry,最外面套 timeit。执行顺序是:timeit 开始计时 → retry 控制重试 → 失败时 log_on_failure 打日志。

小结

这 10 个装饰器不是什么高深理论,全是实际测试工作里遇到的真实问题的解法:

都是直接拿走能用的代码,根据自己项目的 URL 和字段改一改就行。‍

文章写完了,整理一下这 10 个的思路:

从最常用的痛点切入,每个装饰器都是真实场景里会碰到的问题,不是为了介绍语法而凑数的:

@retry — 接口偶发超时,自动重试,不用手写循环

@timeit — 接口耗时统计,一行代码加上去,性能问题一目了然

@with_token — 自动登录注入 token,多个用例不用反复写登录逻辑

@log_on_failure — 失败时自动打出详细堆栈,不用盲猜为什么挂

@skip_in_env — 破坏性测试绝不跑到生产,人工误操作的最后一道门

@validate_schema — 状态码 200 不代表结构对了,字段级别的校验

@timeout — 防止用例卡死,线程级强制超时

@cleanup — 测试完自动清理脏数据,finally 保证一定执行

@record_http — 拦截所有请求自动记录,测试报告直接拿走

@concurrent — 快速并发压测,不装专门的工具也能用‍

 每一次互动,皆是鼓励, 每一份支持,共促成长。

商务合作:RYXtest

最新文章

随机文章