1. 引言
很多同学学完 Selenium、Pytest、Requests 之后,依然不知道如何把它们串成一个真正能用的项目。知识点是散的,面试时能说,工作中却落不了地。
本文以一个「电商后台管理系统」为被测对象,从零搭建一套完整的自动化测试项目,覆盖:
- 项目目录结构设计
- 接口自动化(Requests + Pytest)
- UI 自动化(Selenium + Page Object)
- 数据驱动与配置管理
- 测试报告与持续集成
- 常见坑与优化建议
读完之后,你应该能独立完成一个可以写进简历、也能直接搬到工作中的自动化测试项目。
2. 项目整体设计
2.1 被测对象与技术选型
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 接口层 | Requests + Pytest | 覆盖登录、商品、订单等核心接口 |
| UI 层 | Selenium + Pytest | 覆盖登录、下单等关键业务流程 |
| 数据管理 | YAML + Faker | 配置与测试数据分离 |
| 报告 | Allure | 生成可视化测试报告 |
| 持续集成 | Jenkins / GitHub Actions | 定时执行与结果通知 |
2.2 目录结构
text
auto-test-project/
├── config/
│ ├── config.yaml # 环境配置
│ └── __init__.py
├── data/
│ ├── login_data.yaml # 测试数据
│ └── order_data.yaml
├── common/
│ ├── request_client.py # 接口请求封装
│ ├── logger.py # 日志封装
│ ├── yaml_util.py # YAML 读取
│ └── assert_util.py # 断言工具
├── api/
│ ├── login_api.py
│ ├── product_api.py
│ └── order_api.py
├── pages/
│ ├── base_page.py
│ ├── login_page.py
│ └── order_page.py
├── testcases/
│ ├── api/
│ │ ├── test_login.py
│ │ └── test_order.py
│ └── ui/
│ └── test_login_ui.py
├── conftest.py
├── pytest.ini
├── requirements.txt
└── run.py
分层清晰是这个项目的第一原则:配置、数据、公共方法、业务封装、测试用例各司其职,后续维护成本会低很多。
3. 环境准备
3.1 依赖安装
bash
pip install pytest requests selenium pyyaml allure-pytest faker
requirements.txt 建议固定版本,避免 CI 环境漂移:
text
pytest==8.2.0
requests==2.32.0
selenium==4.21.0
PyYAML==6.0.1
allure-pytest==2.13.5
Faker==25.2.0
3.2 配置文件
config/config.yaml:
yaml
env: test
test:
base_url: "http://test.example.com/api"
ui_url: "http://test.example.com"
username: "admin"
password: "123456"
prod:
base_url: "http://prod.example.com/api"
ui_url: "http://prod.example.com"
username: "admin"
password: "******"
读取配置的封装:
python
# common/yaml_util.py
import yaml
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
def load_yaml(file_path: str) -> dict:
path = BASE_DIR / file_path
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def get_config() -> dict:
config = load_yaml("config/config.yaml")
env = config["env"]
return config[env]
通过环境变量切换环境
把 env 写死在 config.yaml 里,本地和 CI 就得来回改文件,很容易误提交。更稳妥的做法是让环境变量优先:本地默认读 test,CI 里通过 TEST_ENV=prod 之类的变量覆盖。
python
# common/yaml_util.py
import os
import yaml
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
def load_yaml(file_path: str) -> dict:
path = BASE_DIR / file_path
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def get_config() -> dict:
config = load_yaml("config/config.yaml")
# 环境变量优先,未设置时回退到配置文件中的 env
env = os.getenv("TEST_ENV", config.get("env", "test"))
if env not in config:
raise ValueError(f"未找到环境配置: {env},请检查 config.yaml 或 TEST_ENV")
return config[env]
使用方式:
bash
# 本地默认走 test
pytest testcases
# CI 或临时切换环境
TEST_ENV=prod pytest testcases
在 GitHub Actions 中只需给 job 加一个环境变量:
yaml
jobs:
test:
runs-on: ubuntu-latest
env:
TEST_ENV: test
steps:
- uses: actions/checkout@v4
# ...
敏感信息不要明文写在配置文件里
config.yaml 会随代码一起提交到仓库,生产环境的账号密码一旦写进去就等于公开。建议:
- 配置文件里只保留非敏感项(
base_url、ui_url、username),密码字段留空或写占位符; - 密码统一从环境变量读取,例如
TEST_PASSWORD,代码中做兜底:
python
def get_config() -> dict:
config = load_yaml("config/config.yaml")
env = os.getenv("TEST_ENV", config.get("env", "test"))
if env not in config:
raise ValueError(f"未找到环境配置: {env}")
cfg = dict(config[env])
# 环境变量中的密码优先级最高
cfg["password"] = os.getenv("TEST_PASSWORD", cfg.get("password", ""))
return cfg
- CI 中使用仓库 Secrets(如
${``{ secrets.TEST_PASSWORD }})注入,本地用.env或系统环境变量,.env记得加入.gitignore; - 生产环境尽量使用独立的只读测试账号,避免用管理员账号跑自动化。
4. 接口自动化实战
4.1 请求客户端封装
直接在每个用例里写 requests.post(...) 会导致大量重复代码,也不方便统一加日志、加 token。封装一层:
python
# common/request_client.py
import requests
from common.logger import logger
class RequestClient:
def __init__(self, base_url: str, token: str = None):
self.base_url = base_url
self.session = requests.Session()
if token:
self.session.headers.update({"Authorization": f"Bearer {token}"})
def request(self, method: str, url: str, **kwargs):
full_url = self.base_url + url
logger.info(f"请求: {method.upper()} {full_url}, 参数: {kwargs}")
resp = self.session.request(method, full_url, timeout=10, **kwargs)
logger.info(f"响应: {resp.status_code} {resp.text[:200]}")
return resp
def get(self, url, **kwargs):
return self.request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.request("POST", url, **kwargs)
4.2 业务接口封装
python
# api/login_api.py
from common.request_client import RequestClient
class LoginApi:
def __init__(self, client: RequestClient):
self.client = client
def login(self, username: str, password: str):
return self.client.post(
"/login",
json={"username": username, "password": password}
)
4.3 测试用例与数据驱动
data/login_data.yaml:
yaml
- case_name: "正常登录"
username: "admin"
password: "123456"
expect_code: 200
expect_msg: "success"
- case_name: "密码错误"
username: "admin"
password: "wrong"
expect_code: 401
expect_msg: "用户名或密码错误"
- case_name: "用户名为空"
username: ""
password: "123456"
expect_code: 400
expect_msg: "用户名不能为空"
python
# testcases/api/test_login.py
import pytest
from common.yaml_util import load_yaml
from api.login_api import LoginApi
login_cases = load_yaml("data/login_data.yaml")
class TestLogin:
@pytest.mark.parametrize("case", login_cases, ids=[c["case_name"] for c in login_cases])
def test_login(self, client, case):
api = LoginApi(client)
resp = api.login(case["username"], case["password"])
body = resp.json()
assert resp.status_code == case["expect_code"]
assert body["msg"] == case["expect_msg"]
4.4 conftest 统一管理 fixture
python
# conftest.py
import pytest
from common.yaml_util import get_config
from common.request_client import RequestClient
@pytest.fixture(scope="session")
def config():
return get_config()
@pytest.fixture(scope="session")
def client(config):
return RequestClient(config["base_url"])
@pytest.fixture(scope="session")
def token(client, config):
resp = client.post(
"/login",
json={"username": config["username"], "password": config["password"]}
)
return resp.json()["data"]["token"]
5. UI 自动化实战
5.1 Page Object 模式
UI 自动化的核心痛点是「页面一改,用例全崩」。Page Object 把元素定位和操作封装到页面类里,用例只关心业务动作。
python
# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def find(self, locator):
return self.wait.until(EC.presence_of_element_located(locator))
def click(self, locator):
self.wait.until(EC.element_to_be_clickable(locator)).click()
def input_text(self, locator, text):
el = self.find(locator)
el.clear()
el.send_keys(text)
python
# pages/login_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage
class LoginPage(BasePage):
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.CSS_SELECTOR, "button.login-btn")
WELCOME_TEXT = (By.CLASS_NAME, "welcome")
def open(self, url):
self.driver.get(url + "/login")
def login(self, username, password):
self.input_text(self.USERNAME_INPUT, username)
self.input_text(self.PASSWORD_INPUT, password)
self.click(self.LOGIN_BUTTON)
def get_welcome_text(self):
return self.find(self.WELCOME_TEXT).text
5.2 UI 测试用例
python
# testcases/ui/test_login_ui.py
import pytest
from selenium import webdriver
from pages.login_page import LoginPage
@pytest.fixture(scope="class")
def driver():
d = webdriver.Chrome()
d.maximize_window()
yield d
d.quit()
class TestLoginUI:
def test_login_success(self, driver, config):
page = LoginPage(driver)
page.open(config["ui_url"])
page.login(config["username"], config["password"])
assert "欢迎" in page.get_welcome_text()
5.3 无头模式与显式等待
CI 环境没有图形界面,需要开启无头模式:
python
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=options)
同时坚决避免 time.sleep,统一使用显式等待,既稳定又快。
6. 测试报告与持续集成
6.1 Allure 报告
bash
pytest testcases --alluredir=./allure-results
allure serve ./allure-results
在用例中补充步骤与附件,报告可读性会大幅提升:
python
import allure
@allure.feature("登录模块")
class TestLogin:
@allure.story("正常登录")
@allure.title("使用正确账号密码登录成功")
def test_login(self, client, case):
with allure.step("调用登录接口"):
resp = LoginApi(client).login(case["username"], case["password"])
with allure.step("校验响应"):
assert resp.status_code == case["expect_code"]
6.2 pytest.ini 配置
ini
[pytest]
testpaths = testcases
addopts = -v -s --alluredir=./allure-results --clean-alluredir
markers =
api: 接口测试
ui: UI 测试
6.3 接入 CI
以 GitHub Actions 为例:
yaml
name: Auto Test
on:
push:
branches: [ main ]
schedule:
- cron: "0 2 * * *"
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r requirements.txt
- run: pytest testcases/api
- uses: actions/upload-artifact@v4
if: always()
with:
name: allure-results
path: allure-results
7. 常见问题与优化建议
7.1 用例不稳定(Flaky)
- UI 层:优先排查是否用了
sleep、是否缺少显式等待、元素是否被遮挡。 - 接口层:检查是否依赖了其他用例产生的数据,尽量让用例自造数据、自清理。
7.2 数据污染
测试数据建议使用 Faker 生成唯一值,例如:
python
from faker import Faker
fake = Faker("zh_CN")
username = f"test_{fake.uuid4()[:8]}"
7.3 执行效率
- 接口用例与 UI 用例分开执行,接口用例作为每次提交的快速门禁。
- UI 用例只保留核心主流程,其余下沉到接口层验证。
- 使用
pytest-xdist并行执行:pytest -n 4。
7.4 分层原则小结
#mermaid-svg-VhuDHNySKQhzzHUb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VhuDHNySKQhzzHUb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VhuDHNySKQhzzHUb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VhuDHNySKQhzzHUb .error-icon{fill:#552222;}#mermaid-svg-VhuDHNySKQhzzHUb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VhuDHNySKQhzzHUb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VhuDHNySKQhzzHUb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VhuDHNySKQhzzHUb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VhuDHNySKQhzzHUb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VhuDHNySKQhzzHUb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VhuDHNySKQhzzHUb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VhuDHNySKQhzzHUb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VhuDHNySKQhzzHUb .marker.cross{stroke:#333333;}#mermaid-svg-VhuDHNySKQhzzHUb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VhuDHNySKQhzzHUb p{margin:0;}#mermaid-svg-VhuDHNySKQhzzHUb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VhuDHNySKQhzzHUb .cluster-label text{fill:#333;}#mermaid-svg-VhuDHNySKQhzzHUb .cluster-label span{color:#333;}#mermaid-svg-VhuDHNySKQhzzHUb .cluster-label span p{background-color:transparent;}#mermaid-svg-VhuDHNySKQhzzHUb .label text,#mermaid-svg-VhuDHNySKQhzzHUb span{fill:#333;color:#333;}#mermaid-svg-VhuDHNySKQhzzHUb .node rect,#mermaid-svg-VhuDHNySKQhzzHUb .node circle,#mermaid-svg-VhuDHNySKQhzzHUb .node ellipse,#mermaid-svg-VhuDHNySKQhzzHUb .node polygon,#mermaid-svg-VhuDHNySKQhzzHUb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VhuDHNySKQhzzHUb .rough-node .label text,#mermaid-svg-VhuDHNySKQhzzHUb .node .label text,#mermaid-svg-VhuDHNySKQhzzHUb .image-shape .label,#mermaid-svg-VhuDHNySKQhzzHUb .icon-shape .label{text-anchor:middle;}#mermaid-svg-VhuDHNySKQhzzHUb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VhuDHNySKQhzzHUb .rough-node .label,#mermaid-svg-VhuDHNySKQhzzHUb .node .label,#mermaid-svg-VhuDHNySKQhzzHUb .image-shape .label,#mermaid-svg-VhuDHNySKQhzzHUb .icon-shape .label{text-align:center;}#mermaid-svg-VhuDHNySKQhzzHUb .node.clickable{cursor:pointer;}#mermaid-svg-VhuDHNySKQhzzHUb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VhuDHNySKQhzzHUb .arrowheadPath{fill:#333333;}#mermaid-svg-VhuDHNySKQhzzHUb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VhuDHNySKQhzzHUb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VhuDHNySKQhzzHUb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VhuDHNySKQhzzHUb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VhuDHNySKQhzzHUb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VhuDHNySKQhzzHUb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VhuDHNySKQhzzHUb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VhuDHNySKQhzzHUb .cluster text{fill:#333;}#mermaid-svg-VhuDHNySKQhzzHUb .cluster span{color:#333;}#mermaid-svg-VhuDHNySKQhzzHUb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VhuDHNySKQhzzHUb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VhuDHNySKQhzzHUb rect.text{fill:none;stroke-width:0;}#mermaid-svg-VhuDHNySKQhzzHUb .icon-shape,#mermaid-svg-VhuDHNySKQhzzHUb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VhuDHNySKQhzzHUb .icon-shape p,#mermaid-svg-VhuDHNySKQhzzHUb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VhuDHNySKQhzzHUb .icon-shape .label rect,#mermaid-svg-VhuDHNySKQhzzHUb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VhuDHNySKQhzzHUb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VhuDHNySKQhzzHUb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VhuDHNySKQhzzHUb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 测试用例层 testcases
业务封装层 api / pages
公共能力层 common
配置与数据层 config / data
被测系统
越靠上的层越贴近业务、越容易变;越靠下的层越稳定、越应复用。遵循这个方向,项目才能长期维护。
8. 总结
一个合格的自动化测试项目,不在于用例数量多,而在于结构清晰、稳定可维护、能持续运行。本文从目录设计、接口封装、Page Object、数据驱动到 CI 集成,给出了一套可以直接落地的骨架。
建议你按下面的顺序动手实践:
- 先跑通一个接口用例,把请求封装和配置读取做扎实;
- 再补充数据驱动,让用例覆盖更多场景;
- 然后引入 UI 自动化,只覆盖核心流程;
- 最后接入 Allure 与 CI,让测试真正「跑起来」。
把这套项目完整做一遍,你对自动化测试的理解会从「会用工具」升级到「能交付工程」。