首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从零构建接口自动化测试框架:Pytest + Excel + Allure + Jenkins 完整实战

从零构建接口自动化测试框架:Pytest + Excel + Allure + Jenkins 完整实战

原创
作者头像
用户12609250
发布2026-07-31 18:11:31
发布2026-07-31 18:11:31
2470
举报

一、为什么需要接口自动化测试框架

在日常迭代中,接口变动频繁,人工回归耗时且容易遗漏。一套设计良好的接口自动化框架,能让测试用例从“一次性脚本”升级为“可复用资产”。

华测教育的课程体系中,接口自动化是核心模块之一,涵盖从 Requests 入门到框架封装、Jenkins 持续集成的完整链路。本文按照“数据驱动 → 核心封装 → 报告集成 → 无人值守”的路线,从零搭建一套可直接运行的接口自动化测试框架。

最终效果:测试用例写在 Excel 中,一条命令完成全部接口测试,自动生成 Allure 报告,并支持 Jenkins 流水线执行。


二、整体架构

代码语言:javascript
复制
┌─────────────────────────────────────────────────────────────┐
│  数据层:Excel 测试用例(接口信息 + 请求参数 + 预期结果)   │
└─────────────────────┬───────────────────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────────────────┐
│  核心层:读取 Excel → 封装 Requests → 执行请求            │
│          → 断言校验(JSONPath/正则/状态码)                │
└─────────────────────┬───────────────────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────────────────┐
│  报告层:Allure 生成结构化测试报告                         │
└─────────────────────┬───────────────────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────────────────┐
│  集成层:Jenkins 流水线自动拉取代码 + 执行测试 + 发报告   │
└─────────────────────────────────────────────────────────────┘

框架核心设计理念是数据与代码分离:Excel 中维护用例数据,Python 代码负责通用的请求封装和断言逻辑,修改用例无需改代码。


三、核心代码实现

3.1 环境准备与项目结构

代码语言:javascript
复制
api_test_framework/
├── config/
│   └── config.yaml              # 全局配置(域名、超时等)
├── data/
│   └── test_cases.xlsx          # Excel 测试用例
├── common/
│   ├── excel_reader.py          # 读取 Excel
│   ├── http_client.py           # Requests 封装
│   └── assert_engine.py         # 断言引擎
├── testcases/
│   └── test_api.py              # pytest 测试入口
├── reports/                     # 测试报告目录
├── conftest.py                  # pytest 钩子(Allure 配置)
├── requirements.txt
└── run.py                       # 执行入口

依赖安装:

代码语言:javascript
复制
pip install pytest requests openpyxl allure-pytest pyyaml

3.2 配置管理

代码语言:javascript
复制
# config/config.yaml
base:
  host: "https://api.example.com"
  timeout: 10

headers:
  Content-Type: "application/json"

# config/loader.py
import yaml
import os

class ConfigLoader:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            config_path = os.path.join(os.path.dirname(__file__), "config.yaml")
            with open(config_path, "r", encoding="utf-8") as f:
                cls._instance.config = yaml.safe_load(f)
        return cls._instance
    
    @property
    def host(self):
        return self.config["base"]["host"]
    
    @property
    def timeout(self):
        return self.config["base"]["timeout"]
    
    def get_headers(self):
        return self.config.get("headers", {})

3.3 Excel 测试用例读取器

Excel 模板设计:

用例ID

模块

接口名称

方法

URL

请求头

请求体

预期状态码

断言方式

断言表达式

期望值

TC001

用户

登录

POST

/login

{"token":"xxx"}

{"username":"test","password":"123"}

200

jsonpath

$.code

0

代码语言:javascript
复制
# common/excel_reader.py
import openpyxl
from typing import List, Dict, Any

class ExcelReader:
    def __init__(self, file_path: str):
        self.file_path = file_path
    
    def read_all_cases(self) -> List[Dict[str, Any]]:
        """读取 Excel 所有用例,返回字典列表"""
        wb = openpyxl.load_workbook(self.file_path)
        ws = wb.active
        
        # 第一行为表头
        headers = [cell.value for cell in ws[1]]
        cases = []
        
        for row in ws.iter_rows(min_row=2, values_only=True):
            case = dict(zip(headers, row))
            # 跳过空行
            if not case.get("用例ID"):
                continue
            # 解析 JSON 格式的请求头和请求体
            if case.get("请求头"):
                case["请求头"] = eval(case["请求头"]) if isinstance(case["请求头"], str) else {}
            if case.get("请求体"):
                case["请求体"] = eval(case["请求体"]) if isinstance(case["请求体"], str) else {}
            if case.get("断言表达式"):
                case["断言表达式"] = str(case["断言表达式"])
            cases.append(case)
        
        wb.close()
        return cases
    
    def get_case_by_id(self, case_id: str) -> Dict:
        """根据用例ID获取单条用例"""
        cases = self.read_all_cases()
        for case in cases:
            if str(case.get("用例ID")) == case_id:
                return case
        return {}

3.4 HTTP 客户端封装

代码语言:javascript
复制
# common/http_client.py
import requests
import json
from typing import Optional, Dict, Any

class HttpClient:
    def __init__(self, base_url: str, timeout: int = 10):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.session = requests.Session()
    
    def _build_url(self, path: str) -> str:
        """拼接完整 URL"""
        if path.startswith("http"):
            return path
        return f"{self.base_url}/{path.lstrip('/')}"
    
    def _parse_body(self, body: Any) -> Dict:
        """将字符串形式的 body 解析为字典"""
        if isinstance(body, dict):
            return body
        if isinstance(body, str):
            try:
                return json.loads(body)
            except json.JSONDecodeError:
                return {}
        return {}
    
    def request(
        self, 
        method: str, 
        path: str, 
        headers: Optional[Dict] = None, 
        body: Optional[Dict] = None
    ) -> requests.Response:
        """统一请求入口"""
        url = self._build_url(path)
        headers = headers or {}
        body = self._parse_body(body)
        
        # 合并默认 headers
        default_headers = {"Content-Type": "application/json"}
        default_headers.update(headers)
        
        method = method.upper()
        if method == "GET":
            return self.session.get(url, params=body, headers=default_headers, timeout=self.timeout)
        elif method == "POST":
            return self.session.post(url, json=body, headers=default_headers, timeout=self.timeout)
        elif method == "PUT":
            return self.session.put(url, json=body, headers=default_headers, timeout=self.timeout)
        elif method == "DELETE":
            return self.session.delete(url, json=body, headers=default_headers, timeout=self.timeout)
        else:
            raise ValueError(f"不支持的请求方法: {method}")

3.5 断言引擎

支持状态码断言JSONPath 断言两种方式。

代码语言:javascript
复制
# common/assert_engine.py
import json
from typing import Any, Dict
from jsonpath import jsonpath  # pip install jsonpath

class AssertEngine:
    @staticmethod
    def assert_status_code(response, expected_code: int) -> bool:
        """断言状态码"""
        return response.status_code == expected_code
    
    @staticmethod
    def assert_jsonpath(response, expr: str, expected_value: Any) -> bool:
        """
        JSONPath 断言
        示例: expr="$.code", expected_value=0
        """
        try:
            data = response.json()
            result = jsonpath(data, expr)
            if not result:
                return False
            # 如果期望值是字符串,转成字符串比较;否则直接比较
            if isinstance(expected_value, str) and expected_value.isdigit():
                expected_value = int(expected_value)
            return result[0] == expected_value
        except Exception as e:
            print(f"JSONPath 断言执行失败: {e}")
            return False
    
    @staticmethod
    def assert_regex(response, pattern: str, expected_value: str) -> bool:
        """正则断言"""
        import re
        match = re.search(pattern, response.text)
        if not match:
            return False
        return match.group(0) == expected_value
    
    def assert_case(self, response, case: Dict) -> tuple:
        """
        执行单条用例的所有断言
        返回: (是否通过, 详细结果)
        """
        results = []
        all_passed = True
        
        # 1. 状态码断言
        expected_code = case.get("预期状态码", 200)
        status_ok = self.assert_status_code(response, expected_code)
        results.append(f"状态码: {response.status_code} (期望 {expected_code}) {'✅' if status_ok else '❌'}")
        if not status_ok:
            all_passed = False
        
        # 2. JSONPath 断言
        if case.get("断言方式") == "jsonpath":
            expr = case.get("断言表达式")
            expected = case.get("期望值")
            if expr and expected:
                json_ok = self.assert_jsonpath(response, expr, expected)
                results.append(f"JSONPath: {expr} -> {expected} {'✅' if json_ok else '❌'}")
                if not json_ok:
                    all_passed = False
        
        # 3. 正则断言
        if case.get("断言方式") == "regex":
            pattern = case.get("断言表达式")
            expected = case.get("期望值")
            if pattern and expected:
                regex_ok = self.assert_regex(response, pattern, expected)
                results.append(f"正则: {pattern} {'✅' if regex_ok else '❌'}")
                if not regex_ok:
                    all_passed = False
        
        return all_passed, "\n".join(results)

3.6 Pytest 测试入口

代码语言:javascript
复制
# conftest.py
import pytest
import allure
from common.excel_reader import ExcelReader
from common.http_client import HttpClient
from config.loader import ConfigLoader

@pytest.fixture(scope="session")
def config():
    return ConfigLoader()

@pytest.fixture(scope="session")
def http_client(config):
    return HttpClient(config.host, config.timeout)

@pytest.fixture
def test_cases():
    """加载所有测试用例"""
    reader = ExcelReader("data/test_cases.xlsx")
    return reader.read_all_cases()


# testcases/test_api.py
import pytest
import allure
from common.assert_engine import AssertEngine
from common.excel_reader import ExcelReader

@allure.feature("接口自动化测试")
class TestAPI:
    
    @pytest.mark.parametrize(
        "case", 
        ExcelReader("data/test_cases.xlsx").read_all_cases(),
        ids=lambda case: f"{case.get('用例ID')}_{case.get('接口名称')}"
    )
    @allure.story("执行 Excel 中的测试用例")
    def test_api_case(self, http_client, case):
        """
        执行单条 Excel 用例
        """
        with allure.step(f"请求: {case.get('方法')} {case.get('URL')}"):
            response = http_client.request(
                method=case.get("方法", "GET"),
                path=case.get("URL"),
                headers=case.get("请求头", {}),
                body=case.get("请求体", {})
            )
        
        # 记录请求和响应
        allure.attach(
            f"请求: {case.get('方法')} {case.get('URL')}\n请求体: {case.get('请求体')}",
            name="请求信息",
            attachment_type=allure.attachment_type.TEXT
        )
        allure.attach(
            f"状态码: {response.status_code}\n响应体: {response.text[:500]}",
            name="响应信息",
            attachment_type=allure.attachment_type.TEXT
        )
        
        with allure.step("执行断言"):
            engine = AssertEngine()
            passed, detail = engine.assert_case(response, case)
            assert passed, f"断言失败:\n{detail}"

3.7 一键执行入口 import pytest import sys import os def run_tests(allure_report: bool = True): """ 执行全部测试 """ args = [ "testcases/", "-v", "--tb=short", "--alluredir=reports/allure_raw", "--maxfail=5", ] # 如果指定了测试模块,只运行指定模块 if len(sys.argv) > 1: args = [sys.argv[1]] + args[1:] exit_code = pytest.main(args) # 生成 Allure 报告 if allure_report and exit_code != 5: # 5 表示没有测试 os.system("allure generate reports/allure_raw -o reports/allure_html --clean") print("\n✅ Allure 报告已生成: reports/allure_html/index.html") return exit_code if __name__ == "__main__": run_tests()


四、运行与报告

4.1 本地执行

代码语言:javascript
复制
# 安装依赖
pip install -r requirements.txt

# 执行测试
python run.py

# 或使用 pytest 命令
pytest testcases/ -v --alluredir=reports/allure_raw

4.2 生成 Allure 报告

代码语言:javascript
复制
# 安装 allure(Mac)
brew install allure

# 生成并打开报告
allure generate reports/allure_raw -o reports/allure_html --clean
allure open reports/allure_html

Allure 报告会展示:

  • 测试用例通过/失败状态
  • 每个接口的请求和响应详情
  • 执行耗时统计
  • 按模块/功能分组的测试结果

4.3 Jenkins 持续集成

创建 Jenkinsfile 实现自动化执行:

代码语言:javascript
复制
pipeline {
    agent any
    
    environment {
        PYTHONPATH = "${env.WORKSPACE}"
    }
    
    stages {
        stage('代码拉取') {
            steps {
                git branch: 'main', 
                    url: 'https://github.com/your/api_test_framework.git'
            }
        }
        
        stage('环境准备') {
            steps {
                sh 'pip install -r requirements.txt'
            }
        }
        
        stage('执行接口测试') {
            steps {
                sh '''
                    pytest testcases/ -v --alluredir=reports/allure_raw
                '''
            }
            post {
                always {
                    // 生成 Allure 报告
                    allure([
                        includeProperties: false,
                        jdk: '',
                        properties: [],
                        reportBuildPolicy: 'ALWAYS',
                        results: [[path: 'reports/allure_raw']]
                    ])
                }
            }
        }
    }
    
    post {
        failure {
            emailext (
                subject: "接口自动化测试失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: "请查看测试报告: ${env.BUILD_URL}allure",
                to: "qa-team@example.com"
            )
        }
    }
}

配置 Jenkins 定时执行(如每日凌晨 2 点):

代码语言:javascript
复制
H 2 * * *

这样就实现了无人值守的接口自动化测试:Jenkins 定时拉取最新代码 → 执行测试 → 生成 Allure 报告 → 失败时邮件通知。


五、框架扩展方向

5.1 支持关联接口

接口依赖是常见场景(如登录后获取 token,后续接口携带 token)。可在 Excel 中增加“依赖接口”字段,框架自动维护变量池:

代码语言:javascript
复制
# 在 http_client 中增加变量池
class HttpClient:
    def __init__(self, ...):
        self.variables = {}  # 存储全局变量
    
    def set_var(self, key, value):
        self.variables[key] = value
    
    def get_var(self, key):
        return self.variables.get(key)

用例中支持 ${token} 占位符,执行前替换。

5.2 支持加密接口

对于 MD5、AES 加密接口,可在请求前调用加密函数:

代码语言:javascript
复制
def encrypt_body(body: dict, secret: str) -> dict:
    # 根据业务规则对 body 进行加密
    return body

5.3 集成测试覆盖率统计

参考 RTC 闭环覆盖率的思路,可将测试用例与需求、代码建立追溯关系,在测试报告中加入覆盖率指标。


六、效果总结

指标

人工测试

框架自动化

单轮回归耗时

2-4 小时

3-5 分钟

用例维护成本

每次改代码需重新测试

仅修改 Excel

报告生成

手工整理

Allure 自动生成

定时执行

需要人工触发

Jenkins 无人值守

核心收益:将重复性的回归测试交给框架,测试人员将精力集中在用例设计缺陷分析上。


参考资源

  • 华测教育接口自动化系列课程
  • 基于 RTC 闭环覆盖率的自动化测试系统研究
  • Postman + Jenkins + Allure 接口自动化架构实践

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、为什么需要接口自动化测试框架
  • 二、整体架构
  • 三、核心代码实现
    • 3.1 环境准备与项目结构
    • 3.2 配置管理
    • 3.3 Excel 测试用例读取器
    • 3.4 HTTP 客户端封装
    • 3.5 断言引擎
    • 3.6 Pytest 测试入口
    • 3.7 一键执行入口 import pytest import sys import os def run_tests(allure_report: bool = True): """ 执行全部测试 """ args = [ "testcases/", "-v", "--tb=short", "--alluredir=reports/allure_raw", "--maxfail=5", ] # 如果指定了测试模块,只运行指定模块 if len(sys.argv) > 1: args = [sys.argv[1]] + args[1:] exit_code = pytest.main(args) # 生成 Allure 报告 if allure_report and exit_code != 5: # 5 表示没有测试 os.system("allure generate reports/allure_raw -o reports/allure_html --clean") print("\n✅ Allure 报告已生成: reports/allure_html/index.html") return exit_code if __name__ == "__main__": run_tests()
  • 四、运行与报告
    • 4.1 本地执行
    • 4.2 生成 Allure 报告
    • 4.3 Jenkins 持续集成
  • 五、框架扩展方向
    • 5.1 支持关联接口
    • 5.2 支持加密接口
    • 5.3 集成测试覆盖率统计
  • 六、效果总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档