从零开始接口自动化测试框架搭建

写在前面

做了两三年功能测试的同学,大概率会遇到这样的场景:

  • 每次发版,回归用例两百多条,手工点点点跑到怀疑人生
  • 改了一个字段,不确定影响范围,只能把相关接口全跑一遍
  • 上线后发现接口挂了,而你的测试用例里明明覆盖了——但你不确定是不是最新版本的数据跑的

这些问题,接口自动化都能解决。不是那种”学了面试能吹一吹”的解决,而是实打实地每天帮你省两三个小时的那种解决。

但很多同学想学,打开网上一搜,各种框架五花八门,看完还是不知道怎么开始。今天这篇文章,就带你从零开始、一步步搭一套完整的接口自动化框架

不用花哨的东西,使用 Python + Pytest + Requests,实用、好懂、能落地


一、框架整体设计思路

在动手写代码之前,先想清楚一件事:框架要分层

什么是分层?就是让不同的事情各司其职:

层级职责举个例子
用例层只管写测试场景登录接口正常登录、密码错误登录
业务层封装接口调用逻辑login(username, password)
请求层封装 HTTP 请求发 GET/POST 请求、统一处理响应
数据层管理测试数据和配置URL、环境变量、用例数据

为什么要分这么细?改一个地方不影响其他地方

比如接口地址变了,只改配置文件;比如请求要加统一的 header,只改请求封装;比如要加新用例,只加用例文件。分层之后,维护成本会大幅降低。

一句话总结:用例只关心”测什么”,不关心”怎么发请求”。


二、技术选型

技术选型这件事,纠结的人特别多。其实对于大多数团队来说,答案很简单:

Python

测试领域第一语言。生态好、学习成本低、上手快。就算你之前只会一点 Java 或完全零基础,Python 也比其他语言更容易上手做自动化。

Pytest

Python 测试框架的天花板。比起 unittest:

  • 写法更简洁(不用写 class)
  • 参数化一行搞定(@pytest.mark.parametrize
  • 插件生态强大(Allure 报告、并发执行、重试机制全有)
  • 支持 fixture,做数据准备和清理非常方便

Requests

发 HTTP 请求的库,没有之一。API 设计优雅,几行代码就能搞定一次请求。不用纠结用什么 HTTP 库,Requests 就是标准答案。

YAML / Excel

测试数据管理。YAML 适合配置和环境信息,结构清晰;Excel 适合管理大量测试用例数据,团队中非技术人员也能维护。

为什么不选 Postman/Newman? Postman 适合快速调试和小规模接口测试。但用例多了之后,维护困难、难以做数据驱动、不方便 CI/CD 集成。代码框架的灵活性和可扩展性是 Postman 比不了的。


三、框架目录结构详解

先看整体结构,再逐个解释:

api_autotest/
├── config/                 # 配置文件
│   ├── config.yaml         # 环境配置(URL、数据库等)
│   └── __init__.py
├── data/                   # 测试数据
│   ├── test_login.yaml     # 登录接口测试数据
│   └── __init__.py
├── common/                 # 公共模块
│   ├── base_api.py         # 请求基础封装
│   ├── assertions.py       # 断言封装
│   ├── logger.py           # 日志工具
│   ├── db_handler.py       # 数据库操作(后续扩展)
│   └── __init__.py
├── testcases/              # 测试用例
│   ├── test_login.py       # 登录接口测试
│   └── __init__.py
├── reports/                # 测试报告(自动生成)
├── logs/                   # 日志文件(自动生成)
├── conftest.py             # Pytest 全局 fixture
├── pytest.ini              # Pytest 配置文件
└── requirements.txt        # 依赖清单

各目录作用说明:

  • config/:放环境配置,比如不同环境的接口地址、账号密码等。切换环境只需要改一个文件。
  • data/:测试用例的输入数据和预期结果。数据与用例分离,改数据不用改代码。
  • common/:所有公共能力的封装。请求怎么发、断言怎么做、日志怎么记,统一管理。
  • testcases/:具体的测试用例。每个接口一个文件(或按模块分文件),只写业务逻辑。
  • reports/:Allure 生成的报告存放位置。
  • logs/:运行日志存放位置。
  • conftest.py:Pytest 的全局配置,放 fixture、钩子函数等。
  • pytest.ini:Pytest 的运行配置,指定参数和规则。

四、核心代码实现

下面进入正题,把核心模块一个个写出来。所有代码都是完整可运行的。

4.1 配置文件

先建一个 YAML 配置文件,统一管理环境信息:

# config/config.yaml
env:test

test:
base_url:"https://reqres.in/api"
timeout:10

uat:
base_url:"https://uat.example.com/api"
timeout:10

production:
base_url:"https://api.example.com/api"
timeout:30

然后用一个工具函数来读取配置:

# common/config_reader.py
import yaml
import os

classConfigReader:
def__init__(self):
        config_path = os.path.join(os.path.dirname(__file__), '..', 'config', 'config.yaml')
withopen(config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)

defget(self, key, default=None):
"""获取指定配置项"""
returnself.config.get(key, default)

defget_env_config(self):
"""获取当前环境的配置"""
        env = self.config.get('env', 'test')
returnself.config.get(env, {})

# 全局实例,直接导入使用
conf = ConfigReader()

4.2 请求封装

这是框架的核心。对 Requests 做二次封装,统一处理请求头、超时、日志和异常:

# common/base_api.py
import requests
import logging
from common.config_reader import conf

logger = logging.getLogger(__name__)

classBaseAPI:
def__init__(self):
self.base_url = conf.get_env_config().get('base_url', '')
self.timeout = conf.get_env_config().get('timeout', 10)
self.session = requests.Session()
# 默认请求头,所有请求都会带上
self.session.headers.update({
'Content-Type': 'application/json',
        })

defsend_request(self, method, path, **kwargs):
"""
        统一请求入口
        :param method: GET/POST/PUT/DELETE
        :param path: 接口路径,如 /users
        :param kwargs: requests 支持的所有参数(params, json, data, headers等)
        """
        url = self.base_url + path

# 设置默认超时,允许单个用例覆盖
        kwargs.setdefault('timeout', self.timeout)

        logger.info(f"请求 >>> {method}{url}")
if kwargs.get('json'):
            logger.info(f"请求体 >>> {kwargs['json']}")

try:
            response = self.session.request(method, url, **kwargs)
            logger.info(f"响应 <<< 状态码: {response.status_code}")
            logger.info(f"响应体 <<< {response.text[:500]}")
return response
except requests.exceptions.Timeout:
            logger.error(f"请求超时: {method}{url}")
raise
except requests.exceptions.ConnectionError:
            logger.error(f"连接失败: {method}{url}")
raise

defget(self, path, **kwargs):
returnself.send_request('GET', path, **kwargs)

defpost(self, path, **kwargs):
returnself.send_request('POST', path, **kwargs)

defput(self, path, **kwargs):
returnself.send_request('PUT', path, **kwargs)

defdelete(self, path, **kwargs):
returnself.send_request('DELETE', path, **kwargs)

4.3 测试数据管理

用 YAML 文件管理测试数据,实现数据与代码分离

# data/test_login.yaml
login_success:
username:"eve.holt@reqres.in"
password:"cityslicka"
expected_code:200
expected_key:"token"

login_fail_wrong_password:
username:"eve.holt@reqres.in"
password:"wrong_password"
expected_code:400
expected_message:"Invalid credentials"

login_fail_missing_password:
username:"eve.holt@reqres.in"
password:""
expected_code:400
expected_message:"Missing password"

读取测试数据的工具:

# common/data_loader.py
import yaml
import os

defload_yaml_data(filename):
"""加载 YAML 测试数据文件"""
    file_path = os.path.join(os.path.dirname(__file__), '..', 'data', filename)
withopen(file_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)

4.4 断言封装

把常用的断言封装起来,让用例里的断言代码更简洁,而且报错信息更友好:

# common/assertions.py
import logging

logger = logging.getLogger(__name__)

defassert_status_code(response, expected_code):
"""断言响应状态码"""
    actual_code = response.status_code
assert actual_code == expected_code, \
f"状态码不匹配: 期望 {expected_code}, 实际 {actual_code}"

defassert_response_contains(response, key):
"""断言响应体包含指定字段"""
    data = response.json()
assert key in data, \
f"响应体中缺少字段 '{key}',实际响应: {data}"

defassert_equal(actual, expected, msg=""):
"""断言相等"""
assert actual == expected, \
f"{msg} 期望: {expected}, 实际: {actual}"if msg else \
f"值不匹配: 期望 {expected}, 实际 {actual}"

defassert_response_time(response, max_seconds):
"""断言响应时间"""
    elapsed = response.elapsed.total_seconds()
assert elapsed <= max_seconds, \
f"响应超时: {elapsed}s > {max_seconds}s"

4.5 测试用例编写

前面所有封装都是为了这一步——用例写得干净、清爽

# testcases/test_login.py
import pytest
from common.base_api import BaseAPI
from common.assertions import assert_status_code, assert_response_contains, assert_equal
from common.data_loader import load_yaml_data
from common.logger import setup_logger

# 加载测试数据
test_data = load_yaml_data('test_login.yaml')
setup_logger()

classTestLogin:
"""登录接口测试"""

defsetup_class(self):
self.api = BaseAPI()

deftest_login_success(self):
"""正常登录 - 验证返回 token"""
        data = test_data['login_success']

        response = self.api.post('/login', json={
'email': data['username'],
'password': data['password']
        })

        assert_status_code(response, data['expected_code'])
        assert_response_contains(response, data['expected_key'])
print(f"\n 登录成功,获取到 token: {response.json()['token']}")

deftest_login_wrong_password(self):
"""密码错误登录失败"""
        data = test_data['login_fail_wrong_password']

        response = self.api.post('/login', json={
'email': data['username'],
'password': data['password']
        })

        assert_status_code(response, data['expected_code'])
        assert_equal(
            response.json().get('error'),
            data['expected_message'],
"错误信息不匹配"
        )

deftest_login_missing_password(self):
"""缺少密码参数"""
        data = test_data['login_fail_missing_password']

        response = self.api.post('/login', json={
'email': data['username'],
'password': data['password']
        })

        assert_status_code(response, data['expected_code'])
        assert_equal(
            response.json().get('error'),
            data['expected_message'],
"错误信息不匹配"
        )

deftest_login_response_time(self):
"""响应时间不应超过 2 秒"""
        data = test_data['login_success']

        response = self.api.post('/login', json={
'email': data['username'],
'password': data['password']
        })

        assert_response_time(response, 2)

4.6 日志配置

没有日志的框架等于没有监控的眼睛:

# common/logger.py
import logging
import os

defsetup_logger():
"""配置全局日志"""
    log_dir = os.path.join(os.path.dirname(__file__), '..', 'logs')
    os.makedirs(log_dir, exist_ok=True)

    formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )

# 文件日志
    file_handler = logging.FileHandler(
        os.path.join(log_dir, 'test_run.log'), encoding='utf-8'
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)

# 控制台日志
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)

# 根日志器配置
    root_logger = logging.getLogger()
    root_logger.setLevel(logging.DEBUG)
    root_logger.addHandler(file_handler)
    root_logger.addHandler(console_handler)

4.7 Pytest 配置

# pytest.ini
[pytest]
testpaths = testcases
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v -s --tb=short

五、运行和报告生成

5.1 安装依赖

# requirements.txt
pytest==8.1.1
requests==2.31.0
PyYAML==6.0.1
allure-pytest==2.13.5

执行安装:

pip install -r requirements.txt

5.2 运行测试

# 运行所有用例
pytest

# 运行指定文件
pytest testcases/test_login.py

# 运行指定用例
pytest testcases/test_login.py::TestLogin::test_login_success

# 失败时自动重试(需安装 pytest-rerunfailures)
pytest --reruns 2

# 并发执行(需安装 pytest-xdist)
pytest -n auto

5.3 生成 Allure 报告

Allure 是目前最好看的测试报告框架,没有之一:

# 生成 Allure 数据(在 reports/allure-raw 目录)
pytest --alluredir=reports/allure-raw

# 启动报告服务(浏览器自动打开)
allure serve reports/allure-raw

Allure 报告支持的特性包括:

  • 用例通过率统计
  • 历史趋势图
  • 用例按功能模块分组
  • 附件(请求日志、响应截图)
  • 失败用例自动归类
  • 标签管理(冒烟测试、回归测试等)

如果想在 CI/CD 中集成,只需在 Jenkins 或 GitLab CI 中加上相同的命令即可。


六、框架扩展方向

基础框架搭好之后,根据实际需求逐步扩展:

数据库操作

测试前后需要准备和清理数据时,封装数据库操作:

# common/db_handler.py
import pymysql

classDBHandler:
def__init__(self, host, port, user, password, database):
self.conn = pymysql.connect(
            host=host, port=port, user=user,
            password=password, database=database,
            charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor
        )

defexecute(self, sql, params=None):
withself.conn.cursor() as cursor:
            cursor.execute(sql, params)
self.conn.commit()
return cursor.fetchall()

defclose(self):
self.conn.close()

邮件通知

测试跑完自动发邮件通知团队。

数据驱动

用 @pytest.mark.parametrize 或 Excel 实现数据驱动测试,一套代码跑多组数据。

关联处理

从响应中提取 token、session 等动态数据,传递给后续请求。


七、学习路径建议

框架搭完不是终点,而是起点。建议的学习路径:

第一步:先跑通

把这篇文章的代码全部敲一遍,跑通第一个用例。不要复制粘贴,自己敲一遍才能理解每个文件的作用。

跑通的标准:

  • 能成功发送一个 HTTP 请求
  • 能看到日志输出
  • 能看到测试报告

第二步:再加接口

把你工作中最常见的 3-5 个接口加进去。比如:

  • 登录接口
  • 查询接口
  • 新增接口
  • 修改接口
  • 删除接口

每个接口都按照 test_login.py 的模式来写,慢慢就熟悉了。

第三步:再学参数化

一个接口要测多种场景,比如登录接口:

  • 正常登录
  • 密码错误
  • 账号不存在
  • 密码为空
  • 账号为空

用 @pytest.mark.parametrize 或者 YAML 数据文件,一套代码跑多组数据。

第四步:再学关联

很多接口之间有依赖关系。比如:

  • 登录后拿到 token
  • 下一个接口要用这个 token

学会用 fixture 或者变量来传递动态数据。

第五步:再学 CI/CD

把框架集成到 Jenkins 或 GitLab CI,实现:

  • 代码提交后自动运行测试
  • 测试失败自动发邮件通知
  • 测试报告自动生成和归档

八、常见问题

Q1:为什么要分层?直接写用例不行吗?

可以,但维护成本会很高。接口地址变了,你要改所有用例;请求要加统一 header,你要改所有用例。分层之后,改一处就行。

Q2:为什么用 Pytest 不用 unittest?

Pytest 更简洁、功能更强大、插件生态更丰富。unittest 是 Python 自带的,但写法繁琐,不支持很多现代测试框架的特性。

Q3:为什么用 YAML 不用 Excel?

YAML 结构清晰,支持嵌套,适合配置和环境信息。Excel 适合管理大量测试用例数据,但需要额外的库来读取。两个可以结合使用。

Q4:这个框架能用于实际项目吗?

能。这个框架已经包含了实际项目最核心的部分:请求封装、数据管理、断言封装、日志、报告。扩展方向也给了:数据库操作、邮件通知、数据驱动、关联处理。

Q5:学完这个要多久?

  • 有 Python 基础:1-2 周能跑通并加上自己的接口
  • 没有 Python 基础:先花 1-2 周学 Python 基础,再花 1-2 周学框架

九、总结

这篇文章,我们从零开始搭了一套完整的接口自动化测试框架:

  • 分层设计思路
  • 技术选型(Python + Pytest + Requests + YAML)
  • 目录结构详解
  • 核心代码实现(配置、请求、数据、断言、用例、日志)
  • 运行和报告生成
  • 扩展方向

这套框架不是花架子,而是真正能用于实际项目的。你可以直接拿去用,然后根据自己的需求逐步扩展。

学习自动化测试,最难的不是技术,而是开始。很多人看了无数教程,却从来没有动手写过一行代码。

希望这篇文章能帮你迈出第一步

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注