agentleFS
chenya1123236324chenya1123236324/skills-1/skills/api-testscript-generator/SKILL.md

api-testscript-generator

Skill0 starsChanged 5 months agoMITView on GitHub

接口自动化测试脚本批量生成技能。基于标准化接口定义(api_definitions.json)与可选的全场景测试数据文件,按照团队既定工程规范(Python + Requests + Pytest + Allure2),自动生成分层架构的接口自动化测试脚本。支持数据驱动模式(脚本与数据解耦)和内联数据模式,输出可直接运行的接口自动化脚本工程。

  • Installs packages

The file

Files
SKILL.md

API Test Script Generator - 接口自动化测试脚本批量生成

概述

本技能扮演接口自动化测试架构师角色,核心能力是基于标准化接口定义与可选测试数据,按照团队既定工程规范,批量生成分层架构、数据驱动、可直接运行的接口自动化测试脚本。

两种数据模式:

模式输入数据来源适用场景
数据驱动模式api_definitions.json + 测试数据目录外部 YAML/JSON 数据文件团队规范、数据频繁变更、脚本与数据解耦
内联数据模式api_definitions.json(无测试数据目录)脚本内自动生成默认测试数据快速验证、无测试数据文件、初期搭建

分层架构:

api_auto_project/
├── config/             # 环境配置、全局常量
├── api/                # 接口请求层(封装所有接口)
├── testcases/          # 测试用例层
├── data/               # 测试数据(数据驱动模式)
├── utils/              # 工具类
├── reports/            # 报告输出
├── conftest.py         # Pytest 全局钩子
└── pytest.ini          # Pytest 配置

触发条件

以下场景自动触发本技能:

  • 用户提供 api_definitions.json 或 api_definitions.yaml 文件,要求生成接口自动化脚本
  • 用户同时提供 api_definitions.json + 测试数据目录,要求按数据驱动方式生成脚本
  • 用户要求"生成接口测试脚本""生成自动化脚本""接口自动化代码生成"
  • 用户提及"api-testscript-generator""/api_testscript_generator"
  • 用户需要将接口定义转化为可执行的 Pytest 自动化测试工程
  • 用户需要基于 api-testdata-generator 的输出结果生成对应的测试脚本

输入

必需输入

  1. 标准化接口定义文件:由 api-schema-parser 输出的 api_definitions.json/yaml
    • 包含 meta、apis、global_rules 顶层结构
    • 每个接口含 api_id、path、method、parameters、responses、business_rules

可选输入

  1. 全场景测试数据目录:由 api-testdata-generator 输出的测试数据文件目录
    • 每个接口对应一个 YAML/JSON 数据文件
    • 数据文件包含 test_cases 列表,每条含 case_id、name、category、parameters、expected
    • 目录按模块分组织(如 auth/、order/)
  2. 目标接口/模块筛选(选填):接口 api_id 或模块名称,仅生成指定范围的脚本
  3. 自定义输出路径(选填):指定生成脚本的根目录,默认为当前目录下的 api_auto_project/

执行流程

输入(api_definitions.json + 可选测试数据目录)
        ↓
  Step 1: 读取接口结构与参数约束
        ↓
  Step 2: 识别数据模式(数据驱动 / 内联数据)
        ↓
  Step 3: 生成项目基础设施(config/、utils/、conftest.py、pytest.ini)
        ↓
  Step 4: 生成接口请求层(api/ 层封装)
        ↓
  Step 5: 生成测试数据层(data/ 层,数据驱动模式)
        ↓
  Step 6: 生成测试用例层(testcases/ 层)
        ↓
  Step 7: 注入健壮逻辑(超时、重试、异常捕获、日志)
        ↓
  Step 8: 输出校验与汇总

Step 1: 读取接口结构与参数约束

从 api_definitions.json 中逐个读取接口定义,提取以下关键信息:

信息类型来源字段用途
接口路径path拼接请求 URL
请求方法method确定请求类型(GET/POST/PUT/DELETE)
路径参数parameters.path_paramsURL 路径替换
查询参数parameters.query_params拼接 Query String
请求头parameters.header_params构建 Headers
请求体parameters.body_params构建 Request Body
响应结构responses.success / responses.errors生成断言
业务规则business_rules鉴权、幂等等特殊处理
全局规则global_rules全局鉴权、限流等
模块归属module文件与目录组织

Step 2: 识别数据模式

判定条件数据模式处理方式
用户提供了测试数据目录,且目录下存在对应的 YAML/JSON 文件数据驱动模式将数据文件映射到 data/ 层,用例层通过 @pytest.mark.parametrize 或 yaml.safe_load() 读取
用户未提供测试数据目录,或数据目录为空内联数据模式在 testcases/ 层用例中直接构建测试数据字典,或生成简单的 data/ 层默认数据

数据文件匹配规则(数据驱动模式):

测试数据目录结构对应接口匹配方式
auth/user_login.yamlPOST_/api/auth/login按模块/文件名映射
order/create_order.yamlPOST_/api/order/create按模块/文件名映射

匹配策略:优先按 api_id 精确匹配,其次按接口名称模糊匹配,最后按模块+路径推断。

Step 3: 生成项目基础设施

3.1 config/ 环境配置

config/config.py:

"""
全局配置模块
- 环境切换:dev/test/pre/prod
- 读取对应环境配置文件
"""
import os
import yaml

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

ENV = os.getenv("API_TEST_ENV", "test")

def load_config():
    """加载环境配置"""
    config_path = os.path.join(BASE_DIR, "config", f"{ENV}.yaml")
    with open(config_path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)

CONFIG = load_config()

# 全局常量
BASE_URL = CONFIG.get("base_url", "")
TIMEOUT = CONFIG.get("timeout", 30)
MAX_RETRY = CONFIG.get("max_retry", 2)
CONTENT_TYPE = "application/json"

config/dev.yaml / config/test.yaml:

# test.yaml 示例
base_url: "https://test-api.example.com"
timeout: 30
max_retry: 2

auth:
  login_url: "/api/auth/login"
  username: "testuser"
  password: "Test@1234"

database:
  host: "test-db.example.com"
  port: 3306
  name: "test_db"

3.2 utils/ 工具类

utils/logger.py:

"""
统一日志模块
- 格式:时间 - 级别 - 模块 - 信息
- 请求/响应自动打印
"""
import logging
import os

LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "reports", "logs")
os.makedirs(LOG_DIR, exist_ok=True)

def get_logger(name):
    """获取 Logger 实例"""
    logger = logging.getLogger(name)
    if not logger.handlers:
        logger.setLevel(logging.DEBUG)
        formatter = logging.Formatter(
            "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
        )
        # 控制台输出
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.INFO)
        console_handler.setFormatter(formatter)
        logger.addHandler(console_handler)
        # 文件输出
        file_handler = logging.FileHandler(
            os.path.join(LOG_DIR, f"{name}.log"), encoding="utf-8"
        )
        file_handler.setLevel(logging.DEBUG)
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)
    return logger

utils/request_util.py:

"""
统一请求工具类
- 超时统一 30s
- 失败自动重试 2 次
- 异常捕获(连接超时/读取超时/连接错误/代理异常/数据解析异常)
- 请求/响应自动日志
- Allure 步骤记录
"""
import allure
import requests
from requests.exceptions import (
    ConnectionError,
    ProxyError,
    ReadTimeout,
    ConnectTimeout,
    JSONDecodeError,
)
from utils.logger import get_logger
from config.config import TIMEOUT, MAX_RETRY

logger = get_logger("request_util")

class RequestUtil:
    """统一请求封装"""

    def __init__(self, session=None):
        self.session = session or requests.Session()

    def _retry_request(self, method, url, **kwargs):
        """带重试的请求方法"""
        kwargs.setdefault("timeout", TIMEOUT)
        last_exception = None
        for attempt in range(1, MAX_RETRY + 2):  # 首次 + MAX_RETRY 次重试
            try:
                logger.info(f"[Request] {method} {url} | attempt={attempt}")
                response = self.session.request(method, url, **kwargs)
                logger.info(
                    f"[Response] status={response.status_code} | "
                    f"time={response.elapsed.total_seconds():.3f}s"
                )
                return response
            except (ConnectTimeout, ReadTimeout) as e:
                last_exception = e
                logger.warning(f"[Retry] Timeout on attempt {attempt}: {e}")
            except ConnectionError as e:
                last_exception = e
                logger.warning(f"[Retry] ConnectionError on attempt {attempt}: {e}")
            except ProxyError as e:
                last_exception = e
                logger.warning(f"[Retry] ProxyError on attempt {attempt}: {e}")
            except JSONDecodeError as e:
                last_exception = e
                logger.error(f"[Error] JSONDecodeError: {e}")
                raise
            except Exception as e:
                last_exception = e
                logger.error(f"[Error] Unexpected: {e}")
                raise
        raise last_exception

    @allure.step("GET {url}")
    def get(self, url, **kwargs):
        return self._retry_request("GET", url, **kwargs)

    @allure.step("POST {url}")
    def post(self, url, **kwargs):
        return self._retry_request("POST", url, **kwargs)

    @allure.step("PUT {url}")
    def put(self, url, **kwargs):
        return self._retry_request("PUT", url, **kwargs)

    @allure.step("DELETE {url}")
    def delete(self, url, **kwargs):
        return self._retry_request("DELETE", url, **kwargs)

    @allure.step("PATCH {url}")
    def patch(self, url, **kwargs):
        return self._retry_request("PATCH", url, **kwargs)

utils/assert_util.py:

"""
统一断言工具类
- 三层断言:状态码 + 业务码 + 业务数据
- Allure 步骤记录
- 断言失败自动附加响应信息
"""
import allure
from utils.logger import get_logger

logger = get_logger("assert_util")

class AssertUtil:
    """统一断言封装"""

    @staticmethod
    @allure.step("断言状态码")
    def assert_status_code(response, expected_code):
        """断言 HTTP 状态码"""
        actual_code = response.status_code
        assert actual_code == expected_code, (
            f"状态码断言失败: 期望={expected_code}, 实际={actual_code} | "
            f"响应={response.text[:500]}"
        )

    @staticmethod
    @allure.step("断言业务码")
    def assert_business_code(response, expected_code):
        """断言业务状态码"""
        try:
            json_data = response.json()
        except Exception:
            assert False, f"响应非JSON格式: {response.text[:500]}"
        actual_code = json_data.get("code")
        assert actual_code == expected_code, (
            f"业务码断言失败: 期望={expected_code}, 实际={actual_code} | "
            f"message={json_data.get('message', '')} | "
            f"响应={response.text[:500]}"
        )

    @staticmethod
    @allure.step("断言业务数据")
    def assert_business_data(response, field_checks):
        """
        断言业务数据
        field_checks: list of dict
            [{"field": "data.id", "check": "not_empty"},
             {"field": "data.username", "check": "equals", "expect": "zhangsan"}]
        """
        try:
            json_data = response.json()
        except Exception:
            assert False, f"响应非JSON格式: {response.text[:500]}"

        for check_item in field_checks:
            field_path = check_item["field"]
            check_type = check_item["check"]

            # 按路径取值
            value = json_data
            for key in field_path.split("."):
                if isinstance(value, dict):
                    value = value.get(key)
                elif isinstance(value, list) and key.isdigit():
                    value = value[int(key)]
                else:
                    value = None
                    break

            if check_type == "not_empty":
                assert value is not None and value != "", (
                    f"字段非空断言失败: {field_path} 值为空 | 响应={response.text[:500]}"
                )
            elif check_type == "equals":
                expect = check_item["expect"]
                assert value == expect, (
                    f"字段匹配断言失败: {field_path} 期望={expect}, 实际={value} | "
                    f"响应={response.text[:500]}"
                )
            elif check_type == "type":
                expect_type = check_item["expect"]
                assert isinstance(value, expect_type), (
                    f"类型断言失败: {field_path} 期望类型={expect_type}, 实际类型={type(value)} | "
                    f"实际值={value}"
                )
            elif check_type == "contains":
                expect = check_item["expect"]
                assert expect in str(value), (
                    f"包含断言失败: {field_path} 期望包含={expect}, 实际={value}"
                )
            elif check_type == "length":
                expect_len = check_item["expect"]
                assert len(str(value)) == expect_len, (
                    f"长度断言失败: {field_path} 期望长度={expect_len}, 实际长度={len(str(value))}"
                )

    @staticmethod
    @allure.step("完整三层断言")
    def assert_all(response, expected_status, expected_code=None, field_checks=None):
        """完整三层断言"""
        AssertUtil.assert_status_code(response, expected_status)
        if expected_code is not None:
            AssertUtil.assert_business_code(response, expected_code)
        if field_checks:
            AssertUtil.assert_business_data(response, field_checks)

utils/token_util.py:

"""
统一鉴权与 Token 管理
- 登录接口统一获取 Token
- 全局 Headers 统一注入
- Token 过期自动刷新
"""
import time
from config.config import CONFIG, BASE_URL
from utils.request_util import RequestUtil
from utils.logger import get_logger

logger = get_logger("token_util")

class TokenUtil:
    """Token 管理工具"""

    _token = None
    _token_expire_time = 0
    _token_lifetime = 7200  # 默认 2 小时

    @classmethod
    def get_token(cls, request_util=None):
        """获取 Token,过期自动刷新"""
        if cls._token and time.time() < cls._token_expire_time:
            return cls._token

        logger.info("Token 已过期或未获取,正在刷新...")
        cls.refresh_token(request_util)
        return cls._token

    @classmethod
    def refresh_token(cls, request_util=None):
        """刷新 Token"""
        request_util = request_util or RequestUtil()
        auth_config = CONFIG.get("auth", {})
        login_url = BASE_URL + auth_config.get("login_url", "/api/auth/login")

        login_data = {
            "username": auth_config.get("username", ""),
            "password": auth_config.get("password", ""),
        }

        response = request_util.post(login_url, json=login_data)
        try:
            json_data = response.json()
            cls._token = json_data.get("data", {}).get("token", "")
            cls._token_expire_time = time.time() + cls._token_lifetime
            logger.info("Token 刷新成功")
        except Exception as e:
            logger.error(f"Token 刷新失败: {e}")
            raise

    @classmethod
    def get_headers(cls, request_util=None):
        """获取带 Token 的全局 Headers"""
        token = cls.get_token(request_util)
        return {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        }

    @classmethod
    def clear_token(cls):
        """清除 Token"""
        cls._token = None
        cls._token_expire_time = 0

3.3 conftest.py

"""
Pytest 全局钩子
- Fixture:request_util、auth_headers
- Allure 环境信息
- 失败重跑配置
"""
import pytest
import allure
from utils.request_util import RequestUtil
from utils.token_util import TokenUtil
from config.config import CONFIG, ENV

@pytest.fixture(scope="session")
def request_util():
    """全局请求工具实例"""
    return RequestUtil()

@pytest.fixture(scope="session")
def auth_headers(request_util):
    """全局鉴权 Headers"""
    return TokenUtil.get_headers(request_util)

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """失败截图与响应信息附加"""
    outcome = yield
    report = outcome.get_result()
    if report.when == "call" and report.failed:
        # 附加失败信息到 Allure 报告
        allure.attach(
            str(report.longrepr),
            name="失败详情",
            attachment_type=allure.attachment_type.TEXT,
        )

def pytest_configure(config):
    """Allure 环境信息"""
    allure.environment(
        Environment=ENV,
        Base_URL=CONFIG.get("base_url", ""),
    )

3.4 pytest.ini

[pytest]
testpaths = testcases
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
    -v
    --alluredir=reports/allure-results
    --reruns=1
    --reruns-delay=2
markers =
    smoke: 冒烟测试
    regression: 回归测试
    p0: 优先级P0
    p1: 优先级P1
    p2: 优先级P2

Step 4: 生成接口请求层(api/ 层)

规范要求:

  • 接口只封装在 api/ 层
  • 一个接口对应一个文件
  • 类名采用大驼峰命名法(如 UserAPI、OrderAPI)
  • 方法名采用小写字母+下划线(如 login()、get_user_info())
  • 引入 RequestUtil 和 TokenUtil
  • 每个方法自动处理路径参数替换、Query 参数拼接、Headers 注入、Body 构建

生成模板:

"""
{module_name}接口封装
- 接口路径:{api_path}
- 请求方法:{method}
"""
from config.config import BASE_URL
from utils.request_util import RequestUtil
from utils.token_util import TokenUtil
from utils.logger import get_logger

logger = get_logger("{module_name}_api")


class {ClassName}API:
    """{api_name}接口封装"""

    def __init__(self, request_util=None):
        self.request_util = request_util or RequestUtil()
        self.base_url = BASE_URL

    def {method_name}(self, {params_signature}, headers=None):
        """
        {api_name}
        - 接口路径:{api_path}
        - 请求方法:{method}
        {params_docstring}
        """
        url = self.base_url + "{api_path}"
        {path_param_replace}
        _headers = headers or TokenUtil.get_headers(self.request_util)
        {query_param_build}
        {body_param_build}
        {request_call}
        logger.info(f"{api_name} | status={response.status_code}")
        return response

路径参数替换:

# Path 参数替换
url = url.replace("{id}", str(id))
url = url.replace("{order_id}", str(order_id))

Query 参数构建:

# Query 参数
params = {}
if page is not None:
    params["page"] = page
if size is not None:
    params["size"] = size

Body 参数构建:

# Body 参数
json_data = {
    "username": username,
    "password": password,
}

请求调用(按方法类型):

# GET
response = self.request_util.get(url, headers=_headers, params=params)

# POST
response = self.request_util.post(url, headers=_headers, json=json_data)

# PUT
response = self.request_util.put(url, headers=_headers, json=json_data)

# DELETE
response = self.request_util.delete(url, headers=_headers, params=params)

# PATCH
response = self.request_util.patch(url, headers=_headers, json=json_data)

Step 5: 生成测试数据层(data/ 层)

5.1 数据驱动模式

当用户提供了测试数据目录时,将数据文件映射到 data/ 目录:

data/
├── _dependencies.yaml          # 依赖关系配置(如存在)
├── auth/                       # 认证管理模块
│   ├── user_login_data.yaml    # 登录接口测试数据
│   └── user_register_data.yaml
├── order/
│   ├── create_order_data.yaml
│   └── ...
└── ...

数据文件保持原格式(YAML/JSON),在用例层通过以下方式加载:

import yaml
import os

def load_test_data(data_file):
    """加载 YAML 测试数据"""
    data_path = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
        "data", data_file
    )
    with open(data_path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)

5.2 内联数据模式

当用户未提供测试数据目录时,在 data/ 目录下为每个接口生成默认测试数据文件:

# data/auth/user_login_data.yaml
api_id: "POST_/api/auth/login"
name: "用户登录"
test_cases:
  - case_id: "POS_001"
    name: "合法用户名密码登录"
    category: "positive"
    priority: "P0"
    parameters:
      body_params:
        username: "testuser01"
        password: "Test@1234"
    expected:
      status_code: 200
      business_code: 0
  - case_id: "NEG_001"
    name: "用户名为空"
    category: "negative"
    priority: "P0"
    parameters:
      body_params:
        username: ""
        password: "Test@1234"
    expected:
      status_code: 400

默认生成规则:

  • 正向数据:基于参数的 example/default 值构造合法请求
  • 异常数据:必填参数为空、类型不匹配、超长字符串
  • 仅生成核心场景:不过度生成,保持精简可用

Step 6: 生成测试用例层(testcases/ 层)

规范要求:

  • 用例只写在 testcases/ 层
  • 一个接口对应一个测试文件
  • 测试方法必须以 test_ 开头
  • 类名采用大驼峰命名法,按场景分类(Normal/Exception/Boundary/Security)
  • 引入 pytest、allure、API 层、断言工具
  • 三层断言(状态码 + 业务码 + 业务数据)
  • 数据驱动:使用 @pytest.mark.parametrize 或内部加载

数据驱动模式用例模板:

"""
{api_name}测试用例
- 接口路径:{api_path}
- 请求方法:{method}
"""
import pytest
import allure
import yaml
import os
from api.{module}_api import {ClassName}API
from utils.assert_util import AssertUtil
from utils.logger import get_logger

logger = get_logger("test_{module}_{api}")


class Test{ClassName}Normal:
    """{api_name} - 正向场景"""

    def setup_class(self):
        self.api = {ClassName}API()

    @pytest.mark.p0
    @allure.title("{api_name} - 正常请求")
    @allure.feature("{module_name}")
    @allure.story("正向场景")
    def test_{method_name}_success(self, request_util, auth_headers):
        """测试正常请求"""
        self.api.request_util = request_util
        response = self.api.{method_name}(
            {params_with_defaults},
            headers=auth_headers
        )
        AssertUtil.assert_all(
            response,
            expected_status=200,
            expected_code=0,
            field_checks=[
                {field_check_items}
            ]
        )


class Test{ClassName}Exception:
    """{api_name} - 异常场景"""

    def setup_class(self):
        self.api = {ClassName}API()

    @pytest.mark.p0
    @pytest.mark.parametrize("case", load_test_data("auth/user_login_data.yaml")["test_cases"])
    @allure.title("{api_name} - {case_name}")
    @allure.feature("{module_name}")
    @allure.story("异常场景")
    def test_{method_name}_data_driven(self, case, request_util, auth_headers):
        """数据驱动测试"""
        self.api.request_util = request_util
        # 构建请求参数
        {build_params_from_case}
        response = self.api.{method_name}(
            {params_from_case},
            headers=auth_headers
        )
        # 三层断言
        AssertUtil.assert_status_code(response, case["expected"]["status_code"])
        if "business_code" in case["expected"]:
            AssertUtil.assert_business_code(response, case["expected"]["business_code"])


class Test{ClassName}Boundary:
    """{api_name} - 边界场景"""

    def setup_class(self):
        self.api = {ClassName}API()

    @pytest.mark.p1
    @allure.title("{api_name} - 边界值测试")
    @allure.feature("{module_name}")
    @allure.story("边界场景")
    def test_{method_name}_boundary(self, request_util, auth_headers):
        """测试边界值"""
        # 按接口参数约束生成边界测试
        {boundary_test_cases}


class Test{ClassName}Security:
    """{api_name} - 安全场景"""

    def setup_class(self):
        self.api = {ClassName}API()

    @pytest.mark.p1
    @allure.title("{api_name} - 安全测试")
    @allure.feature("{module_name}")
    @allure.story("安全场景")
    def test_{method_name}_security(self, request_util, auth_headers):
        """测试安全场景"""
        {security_test_cases}

内联数据模式用例模板:

"""
{api_name}测试用例(内联数据模式)
- 接口路径:{api_path}
- 请求方法:{method}
"""
import pytest
import allure
from api.{module}_api import {ClassName}API
from utils.assert_util import AssertUtil
from utils.logger import get_logger

logger = get_logger("test_{module}_{api}")

# 内联测试数据
TEST_DATA = {
    "positive": [
        {
            "name": "合法请求",
            "params": {
                {default_params}
            },
            "expected": {
                "status_code": 200,
                "business_code": 0
            }
        }
    ],
    "negative": [
        {
            "name": "必填参数为空",
            "params": {
                {empty_params}
            },
            "expected": {
                "status_code": 400
            }
        }
    ]
}


class Test{ClassName}Normal:
    """{api_name} - 正向场景"""

    def setup_class(self):
        self.api = {ClassName}API()

    @pytest.mark.p0
    @allure.title("{api_name} - 正常请求")
    @allure.feature("{module_name}")
    @allure.story("正向场景")
    def test_{method_name}_success(self, request_util, auth_headers):
        """测试正常请求"""
        self.api.request_util = request_util
        case = TEST_DATA["positive"][0]
        response = self.api.{method_name}(
            {params_from_case},
            headers=auth_headers
        )
        AssertUtil.assert_all(
            response,
            expected_status=case["expected"]["status_code"],
            expected_code=case["expected"].get("business_code"),
            field_checks=[
                {field_check_items}
            ]
        )

Step 7: 注入健壮逻辑

所有生成的脚本自动包含以下企业级健壮逻辑:

能力实现方式位置
超时统一TIMEOUT=30 常量config/config.py
自动重试MAX_RETRY=2 + 重试循环utils/request_util.py
异常捕获try/except 捕获 5 类异常utils/request_util.py
Token 鉴权TokenUtil 自动获取/刷新utils/token_util.py
失败重跑--reruns=1pytest.ini
日志输出get_logger 统一日志utils/logger.py
Allure 报告步骤/特性/故事标记用例层 @allure
环境切换ENV 环境变量config/config.py
脚本不中断单个用例失败不影响整体Pytest 机制 + try/except

Step 8: 输出校验与汇总

生成完成后,输出汇总报告:

========================================
接口自动化脚本生成汇总
========================================
项目路径:api_auto_project/
接口总数:15
模块数量:3

生成文件统计:
- config/      3 文件(config.py + 2 环境配置)
- api/         15 文件(每个接口一个封装文件)
- testcases/   15 文件(每个接口一个用例文件)
- data/        15 文件(每个接口一个数据文件)
- utils/       4 文件(logger + request + assert + token)
- conftest.py  1 文件
- pytest.ini   1 文件

数据模式:数据驱动 / 内联数据
命名规范:✓ 符合团队标准
断言覆盖:✓ 三层断言(状态码+业务码+业务数据)
健壮逻辑:✓ 超时/重试/异常/鉴权/日志

运行方式:
1. cd api_auto_project
2. pip install -r requirements.txt
3. pytest testcases/ -v --alluredir=reports/allure-results
4. allure serve reports/allure-results
========================================

输出目录结构(完整)

api_auto_project/
├── config/
│   ├── config.py               # 全局配置(环境切换、常量)
│   ├── dev.yaml                # 开发环境配置
│   └── test.yaml               # 测试环境配置
├── api/
│   ├── __init__.py
│   ├── user_api.py             # 用户接口封装
│   ├── auth_api.py             # 认证接口封装
│   ├── order_api.py            # 订单接口封装
│   └── ...                     # 按模块/接口拆分
├── testcases/
│   ├── __init__.py
│   ├── test_user_login.py      # 用户登录用例
│   ├── test_user_register.py   # 用户注册用例
│   ├── test_order_create.py    # 创建订单用例
│   └── ...                     # 按模块/接口拆分
├── data/
│   ├── auth/
│   │   ├── user_login_data.yaml
│   │   └── user_register_data.yaml
│   ├── order/
│   │   ├── create_order_data.yaml
│   │   └── ...
│   └── ...
├── utils/
│   ├── __init__.py
│   ├── logger.py               # 统一日志
│   ├── request_util.py         # 统一请求封装
│   ├── assert_util.py          # 统一断言
│   └── token_util.py           # Token 管理
├── reports/
│   ├── logs/                   # 日志输出目录
│   └── allure-results/         # Allure 报告数据
├── conftest.py                 # Pytest 全局钩子
├── pytest.ini                  # Pytest 配置
├── requirements.txt            # Python 依赖
└── README.md                   # 项目说明(可选)

requirements.txt

requests>=2.28.0
pytest>=7.0.0
allure-pytest>=2.12.0
PyYAML>=6.0
pytest-rerunfailures>=11.0

统一命名规范

包 / 目录

  • 小写字母 + 下划线,简洁清晰
  • 示例:api/、config/、utils/、testcases/、data/

文件名

  • 小写字母 + 下划线,一个接口对应一个文件
  • 示例:user_api.py、order_api.py、test_login.py

类名

  • 大驼峰命名法(UpperCamelCase),按场景分不同类
  • API 层:UserAPI、OrderAPI
  • 用例层:TestUserLoginNormal、TestUserLoginException、TestUserLoginBoundary、TestUserLoginSecurity

方法名

  • 小写字母 + 下划线,语义简短精准
  • API 层:login()、get_user_info()、create_order()
  • 公共方法抽离到 utils/ 目录

测试方法

  • 必须以 test_ 开头,见名知意
  • 示例:test_login_success、test_login_password_error、test_login_mobile_empty

变量名

  • 小写字母 + 下划线,不使用单字母、不使用歧义缩写
  • 示例:username、token、headers、response、order_id

常量

  • 全大写字母 + 下划线,集中存放于配置文件,禁止硬编码
  • 示例:BASE_URL、TIMEOUT、MAX_RETRY、CONTENT_TYPE

断言策略

每条用例至少包含 3 层断言:

层级断言内容示例
1. 状态码断言HTTP 状态码匹配status_code == 200 / status_code == 400
2. 业务码断言业务状态码匹配code == 0 / code == 40001
3. 业务数据断言关键字段非空/匹配/类型/长度data.id not_empty / data.username == "zhangsan"

业务数据断言类型:

检查类型check 值说明
非空检查not_empty字段值不为 None 且不为空字符串
值匹配equals字段值等于期望值
类型检查type字段值类型匹配
包含检查contains字段值包含期望内容
长度检查length字段值长度等于期望值

禁止:

  • 只断言 status_code
  • 无意义断言(如 assert True)
  • 重复冗余断言

全局异常处理

异常类型处理方式
ConnectTimeout自动重试,记录日志
ReadTimeout自动重试,记录日志
ConnectionError自动重试,记录日志
ProxyError自动重试,记录日志
JSONDecodeError直接抛出,记录错误日志
其他异常直接抛出,记录错误日志

重试机制:

  • 默认重试 2 次(首次请求 + 2 次重试 = 最多 3 次请求)
  • 每次重试前记录 warning 日志
  • 重试全部失败后抛出最后一次异常

失败重跑:

  • Pytest --reruns=1,单个用例失败自动重跑 1 次
  • 重跑间隔 2 秒

统一鉴权与 Token 注入

  1. 登录接口统一获取 Token:通过 TokenUtil.get_token() 获取
  2. 全局 Headers 统一注入:Authorization: Bearer {token} + Content-Type: application/json
  3. Token 过期自动刷新:Token 有效期 2 小时,过期自动调用 refresh_token()
  4. 不需要鉴权的接口:在 API 层方法中可传入自定义 headers,跳过 Token 注入

日志、报告、环境切换

日志格式

  • 格式:时间 - 级别 - 模块 - 信息
  • 请求/响应自动打印
  • 控制台输出 INFO 级别,文件输出 DEBUG 级别

报告

  • Allure2 报告
  • 包含步骤(@allure.step)、特性(@allure.feature)、故事(@allure.story)
  • 失败用例自动附加失败详情

环境切换

  • dev/test/pre/prod 四环境隔离
  • 通过环境变量 API_TEST_ENV 切换
  • 命令行:API_TEST_ENV=dev pytest testcases/

生成脚本调用

# 基本用法:仅基于接口定义生成脚本(内联数据模式)
python3 <skill_path>/scripts/testscript_generator.py api_definitions.json --output api_auto_project/

# 数据驱动模式:指定测试数据目录
python3 <skill_path>/scripts/testscript_generator.py api_definitions.json --data-dir test_data/ --output api_auto_project/

# 仅生成指定模块的脚本
python3 <skill_path>/scripts/testscript_generator.py api_definitions.json --module "认证管理" --output api_auto_project/

# 仅生成指定接口的脚本
python3 <skill_path>/scripts/testscript_generator.py api_definitions.json --api "POST_/api/auth/login" --output api_auto_project/

# 指定输出格式为 YAML 数据驱动
python3 <skill_path>/scripts/testscript_generator.py api_definitions.json --data-format yaml --output api_auto_project/

# 跳过基础设施生成(已有项目仅追加接口)
python3 <skill_path>/scripts/testscript_generator.py api_definitions.json --skip-infra --output api_auto_project/

# 不生成 utils/ 和 config/(已有项目仅更新接口和用例)
python3 <skill_path>/scripts/testscript_generator.py api_definitions.json --skip-utils --skip-config --output api_auto_project/

注意事项

  • 严格分层:接口只封装在 api/ 层,用例只写在 testcases/ 层,数据只放 data/ 层,工具统一放 utils/,禁止混用层级、禁止硬编码
  • 命名一致:文件名、类名、方法名、变量名严格遵循命名规范
  • 三层断言:每条用例至少包含状态码 + 业务码 + 业务数据三层断言
  • 数据解耦:数据驱动模式下,测试数据与测试脚本分离,修改数据不影响脚本逻辑
  • 不遗漏接口:api_definitions.json 中的所有接口必须全部生成,不可跳过
  • 不遗漏参数:每个接口的所有参数都必须在 API 封装中体现
  • 业务规则覆盖:识别到的业务规则(鉴权/幂等/限流等)在用例中有对应测试
  • Token 自动处理:需要鉴权的接口自动注入 Token,不需要的接口可跳过
  • 依赖变量处理:测试数据中的 ${VARIABLE} 格式变量在运行时动态替换
  • 路径参数替换:/api/users/{id} 格式的路径参数在 API 层自动替换
  • 编码处理:所有文件 UTF-8 编码
  • 环境隔离:不同环境的配置互不干扰
  • 可运行性:生成的脚本无需修改即可运行(需先配置环境地址和账号)

与其他技能的协作

api-schema-parser ──→ 标准化接口数据 (api_definitions.json)
        │
        ├──→ api-testdata-generator ──→ 测试数据文件 (data/)
        │       │
        │       └──→ api-testscript-generator ──→ 接口自动化脚本工程
        │               │                         (api/ + testcases/ + data/ + utils/ + config/)
        │               │
        │               └── 可直接运行:pytest testcases/
        │
        └──→ api-testscript-generator(无测试数据)
                    │
                    └── 内联数据模式 ──→ 接口自动化脚本工程
                                        (自动生成默认测试数据)

建议使用流程:

  1. 先使用 api-schema-parser 将接口文档标准化为 api_definitions.json
  2. 使用 api-testdata-generator 生成全场景测试数据(可选)
  3. 使用本技能基于接口定义和测试数据生成自动化脚本工程
  4. 配置环境地址和账号后直接运行 pytest testcases/

Resources

scripts/testscript_generator.py

Python 脚本,核心脚本生成引擎,支持:

  • 读取 api_definitions.json 接口定义
  • 识别数据模式(数据驱动/内联数据)
  • 按团队目录规范和分层架构生成脚本文件
  • 自动完成接口请求封装(路径替换、参数构建、请求调用)
  • 按数据驱动方式绑定测试数据
  • 自动生成三层断言
  • 注入企业级健壮逻辑
  • 严格遵循团队编码规范
  • CLI 用法:
    • 基本用法:python3 testscript_generator.py <api_definitions.json> [--data-dir <dir>] [--output <dir>] [--module <name>] [--api <api_id>] [--data-format yaml|json] [--skip-infra] [--skip-utils] [--skip-config]

references/coding-standards.md

团队编码规范参考文档,包含:

  • 目录结构规范
  • 命名规范(包/文件/类/方法/变量/常量)
  • 分层架构规范
  • 硬编码禁止规则
  • 依赖管理规范

references/assertion-patterns.md

断言策略与模式参考文档,包含:

  • 三层断言规范
  • 业务数据断言类型定义
  • 响应字段路径取值规则
  • 断言失败信息格式
  • 禁止断言列表

references/template-reference.md

代码模板参考文档,包含:

  • API 层封装模板
  • 数据驱动用例模板
  • 内联数据用例模板
  • 数据加载工具模板
  • 模块级 __init__.py 模板
  • 依赖变量替换逻辑

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.