1. 项目概述:为什么我们需要一个自己的测试框架?

每次看到团队里测试同学还在手动点点点,或者写着一堆零散、重复、难以维护的脚本,我就觉得是时候聊聊自动化测试框架了。这玩意儿听起来高大上,好像是大厂专利,但其实没那么玄乎。简单说,它就是你给自己和团队搭建的一套“自动化测试工作流”和“工具箱”,目的是把那些重复、枯燥、容易出错的测试任务,用代码和规则管起来,让测试执行更高效,结果更可靠,维护成本更低。

你可能用过 Selenium 录个脚本,或者用 pytest 写几个测试函数,这算自动化,但离“框架”还有距离。一个成型的框架,意味着标准化。比如,所有测试用例的编写有固定的模板和目录结构;测试数据(账号、URL、预期结果)和测试脚本是分离的,改数据不用动代码;测试报告长得都一样,谁都能一眼看懂;环境切换(测试、预发布、生产)只需要改一个配置项。这些好处,在项目初期可能不明显,但随着用例数量从几十个涨到几百上千个,没有框架的团队会陷入“脚本地狱”——找用例像大海捞针,改一个地方要动十个文件,环境一变全盘崩溃。

所以,这个“从0到1搭建高效测试框架”的目标,就是带你一步步走出混沌,用 Python 这门对测试极其友好的语言,构建一个结构清晰、易于扩展、团队协作顺畅的自动化测试基石。无论你是测试开发新手,还是想优化现有流程的资深测试,这套方法都能直接拿来用。

2. 核心需求与设计思路拆解

在动手敲代码之前,我们必须想清楚:这个框架到底要解决哪些痛点?基于常见的团队需求,我总结了以下几个核心设计目标,这也是我们后续所有步骤的指导思想。

2.1 核心需求解析

  1. 用例与代码分离 :测试逻辑(代码)应该和测试数据(输入、预期输出)分开。这样,当业务数据变更时(比如登录账号密码改了),我们只需要更新数据文件,而不是去翻找和修改每一个测试脚本。这是框架可维护性的基石。
  2. 执行灵活性与可配置性 :框架必须能轻松地在不同环境(测试、预发布)下运行,并能灵活地选择执行哪些用例(按模块、按标签、按优先级)。通过配置文件来管理环境变量和全局参数是必须的。
  3. 日志与报告清晰直观 :自动化测试不是运行完就完了。我们需要详尽的执行日志来定位问题,更需要一份美观、信息丰富的测试报告,让项目经理、产品经理等非技术人员也能一目了然地了解测试结果。报告最好能附带截图,对于UI自动化尤其重要。
  4. 失败重试与稳定性处理 :网络波动、页面加载慢、元素偶尔定位不到……这些不稳定因素会导致用例“假失败”。框架应该具备失败自动重试机制,并能在关键步骤失败时自动截图,保存现场证据。
  5. 易于集成与持续运行 :框架最终要融入CI/CD(持续集成/持续部署)流水线。这意味着它应该能通过命令行方便地触发,并且生成机器可读(如JUnit XML格式)和人工可读的报告,方便Jenkins、GitLab CI等工具集成。
  6. 低学习成本与团队协作 :框架结构应该直观,编写新用例的模板应该简单。要建立清晰的目录规范和代码规范,让新成员能快速上手,避免每个人一套写法。

2.2 技术选型与思路

基于以上需求,我们选择 Python 作为主力语言,因为它语法简洁、生态丰富。框架的核心将围绕以下几个明星库搭建:

  • pytest :测试框架的不二之选。它比 unittest 更简洁强大,支持丰富的插件(失败重试、并行执行、html报告等)、灵活的fixture(用于测试前置和后置操作)和参数化,完美契合我们的需求。
  • Selenium :Web UI自动化的标准。我们将用它来驱动浏览器,模拟用户操作。
  • Requests :HTTP接口测试的利器。轻量、高效,用于构建我们的API测试层。
  • Allure pytest-html :用于生成测试报告。Allure报告非常强大美观,但需要额外安装Java环境;pytest-html更轻量,开箱即用。我们将以pytest-html为例,兼顾简便与实用。
  • OpenPyXL / YAML / JSON :用于管理测试数据。根据团队习惯,可以选择Excel、YAML或JSON文件来存储数据。YAML和JSON更受开发者欢迎,因为它们易于版本控制。
  • logging :Python标准库的日志模块,足够记录详细的执行过程。
  • configparser python-dotenv :用于管理配置文件。我们将使用 .ini 文件或 .env 文件来存放环境配置。

整体架构思路是“分层设计”:将测试数据、页面对象(对于UI)、测试用例、通用工具、配置、报告输出等分门别类,放在不同的目录中。这样结构清晰,职责单一。

3. 环境准备与项目初始化

工欲善其事,必先利其器。我们先来把“厨房”收拾好。

3.1 Python与IDE环境搭建

首先,确保你安装了Python(建议3.8及以上版本)。可以从Python官网下载安装包,安装时务必勾选“Add Python to PATH”。安装后,在命令行输入 python --version 验证。

我强烈推荐使用 PyCharm (社区版免费)或 VS Code 作为集成开发环境。它们对Python项目管理和调试的支持非常友好。以VS Code为例,你需要安装Python扩展,并配置好解释器。

接下来,为我们的框架创建一个独立的虚拟环境。这能避免项目间的包版本冲突。在项目根目录下打开终端,执行:

# 创建虚拟环境,环境文件夹名为 venv
python -m venv venv

# 激活虚拟环境
# Windows:
venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate

激活后,命令行提示符前会出现 (venv) 标识。

3.2 核心依赖库安装

在虚拟环境激活的状态下,我们安装框架所需的库。建议使用 requirements.txt 文件来管理依赖。先在项目根目录创建一个 requirements.txt 文件,内容如下:

pytest>=7.0.0
selenium>=4.0.0
requests>=2.28.0
pytest-html>=3.2.0
pytest-rerunfailures>=10.0  # 用于失败重试
openpyxl>=3.0.0  # 用于读取Excel测试数据
PyYAML>=6.0     # 用于读取YAML测试数据
python-dotenv>=0.20.0 # 用于读取.env配置
webdriver-manager>=3.8.0 # 自动管理浏览器驱动

然后,在终端执行安装命令:

pip install -r requirements.txt

webdriver-manager 是个神器,它能自动下载和匹配对应版本的浏览器驱动(如ChromeDriver),省去手动下载配置的麻烦。

3.3 项目目录结构设计

清晰的目录结构是框架的骨架。在项目根目录下,创建如下文件夹和文件:

your_project_name/
├── configs/                 # 配置文件目录
│   ├── __init__.py
│   ├── config.ini          # 主配置文件
│   └── env_config.py       # 环境配置读取类
├── data/                   # 测试数据目录
│   ├── __init__.py
│   ├── test_data.yaml     # 或 test_data.json, test_cases.xlsx
│   └── data_loader.py     # 数据读取工具类
├── page_objects/          # 页面对象模型(PO)目录,UI自动化核心
│   ├── __init__.py
│   ├── base_page.py       # 页面基类
│   └── login_page.py      # 示例:登录页面类
├── test_cases/            # 测试用例目录
│   ├── __init__.py
│   ├── conftest.py        # pytest共享fixture
│   ├── test_api/          # 接口测试用例
│   │   ├── __init__.py
│   │   └── test_user_api.py
│   └── test_ui/           # UI测试用例
│       ├── __init__.py
│       └── test_login.py
├── utils/                 # 通用工具目录
│   ├── __init__.py
│   ├── logger.py          # 日志工具
│   └── common_utils.py    # 其他通用函数
├── reports/               # 测试报告输出目录(.gitignore忽略)
│   └── assets/            # 存放失败截图等
├── logs/                  # 日志文件目录(.gitignore忽略)
├── requirements.txt       # 项目依赖
└── pytest.ini            # pytest配置文件

每个目录下的 __init__.py 文件(可以是空文件)是为了让Python将这个目录视为一个包(package),从而可以相互导入模块。

注意 reports/ logs/ 文件夹因为存放的是每次运行生成的动态文件,应该被添加到 .gitignore 文件中,避免提交到代码仓库。

4. 核心模块实现详解

架子搭好了,现在我们开始砌墙,实现框架最核心的几个模块。

4.1 配置管理模块实现

配置是框架的“开关面板”。我们使用 config.ini 文件来集中管理所有环境相关的变量。 在 configs/config.ini 中写入:

[test]
base_url = https://test.example.com
username = test_user
password = test_pass123
headless = false  # 浏览器是否无头模式运行
browser = chrome
timeout = 10

[prod]
base_url = https://example.com
username = prod_user
password = prod_pass456
headless = true
browser = chrome
timeout = 10

然后,创建 configs/env_config.py 来读取这些配置:

import os
from configparser import ConfigParser
from pathlib import Path

class Config:
    """配置管理类"""
    def __init__(self, env='test'):
        self.config = ConfigParser()
        # 获取config.ini文件的绝对路径
        config_path = Path(__file__).parent / 'config.ini'
        self.config.read(config_path, encoding='utf-8')
        self.env = env

    def get(self, key, section=None):
        """获取配置项"""
        if not section:
            section = self.env
        return self.config.get(section, key)

    def getboolean(self, key, section=None):
        """获取布尔型配置项"""
        if not section:
            section = self.env
        return self.config.getboolean(section, key)

    def getint(self, key, section=None):
        """获取整型配置项"""
        if not section:
            section = self.env
        return self.config.getint(section, key)

# 创建一个全局配置实例,默认使用test环境
# 可以通过环境变量 CONFIG_ENV 来覆盖,方便CI/CD集成
current_env = os.getenv('CONFIG_ENV', 'test')
config = Config(current_env)

这样,在代码的任何地方,我们都可以通过 from configs.env_config import config 来使用 config.get('base_url') 获取当前环境的配置,切换环境只需改一个环境变量。

4.2 日志模块封装

好的日志是调试的救命稻草。我们封装一个简单的日志工具。在 utils/logger.py 中:

import logging
import os
from pathlib import Path
from datetime import datetime

def setup_logger(name='auto_test', log_level=logging.INFO):
    """
    设置并返回一个logger实例
    """
    # 创建logger
    logger = logging.getLogger(name)
    logger.setLevel(log_level)

    # 避免重复添加handler
    if logger.handlers:
        return logger

    # 创建logs目录
    log_dir = Path(__file__).parent.parent / 'logs'
    log_dir.mkdir(exist_ok=True)

    # 日志文件名带日期
    log_file = log_dir / f'test_{datetime.now().strftime("%Y%m%d")}.log'

    # 创建文件handler
    file_handler = logging.FileHandler(log_file, encoding='utf-8')
    file_handler.setLevel(log_level)

    # 创建控制台handler
    console_handler = logging.StreamHandler()
    console_handler.setLevel(log_level)

    # 设置日志格式
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
    )
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)

    # 添加handler到logger
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)

    return logger

# 创建一个全局logger实例
logger = setup_logger()

在测试用例中,导入这个logger: from utils.logger import logger ,然后就可以用 logger.info(“开始登录操作”) logger.error(“元素未找到”) 来记录信息了。日志会同时输出到控制台和按日期分割的文件中,格式里包含了文件名和行号,定位问题非常方便。

4.3 测试数据驱动实现

数据驱动测试是框架的灵魂。这里以YAML格式为例,展示如何将数据与脚本分离。 首先,在 data/test_data.yaml 中定义数据:

login_test:
  positive:
    - username: "correct_user"
      password: "correct_pass"
      expected: "登录成功"
  negative:
    - username: "wrong_user"
      password: "correct_pass"
      expected: “用户名或密码错误”
    - username: "correct_user"
      password: ""
      expected: “密码不能为空”

然后,创建 data/data_loader.py 来加载这些数据:

import yaml
import json
from pathlib import Path

class DataLoader:
    @staticmethod
    def load_yaml(file_name):
        """加载YAML文件"""
        file_path = Path(__file__).parent / file_name
        with open(file_path, 'r', encoding='utf-8') as f:
            return yaml.safe_load(f)

    @staticmethod
    def load_json(file_name):
        """加载JSON文件"""
        file_path = Path(__file__).parent / file_name
        with open(file_path, 'r', encoding='utf-8') as f:
            return json.load(f)

# 示例:获取登录测试数据
test_data = DataLoader.load_yaml('test_data.yaml')
login_positive_cases = test_data['login_test']['positive']

在测试用例中,我们将使用pytest的 @pytest.mark.parametrize 装饰器,配合这些数据来运行多条测试。这样,增加一个新的测试场景,只需要在YAML文件里加一组数据,无需修改测试代码。

4.4 页面对象模型(PO)封装(针对UI测试)

页面对象模型是UI自动化测试的最佳实践,它将页面元素定位和操作封装成类,使测试脚本更清晰,元素变更时只需修改页面类。 首先,在 page_objects/base_page.py 中定义一个所有页面类的基类:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from utils.logger import logger

class BasePage:
    """页面基类,封装通用操作"""
    def __init__(self, driver):
        self.driver = driver
        self.timeout = 10  # 默认等待超时时间

    def find_element(self, locator):
        """查找单个元素,显式等待"""
        try:
            logger.info(f"查找元素: {locator}")
            element = WebDriverWait(self.driver, self.timeout).until(
                EC.presence_of_element_located(locator)
            )
            return element
        except TimeoutException:
            logger.error(f"元素未找到: {locator}")
            # 这里可以添加截图操作
            raise

    def click(self, locator):
        """点击元素"""
        element = self.find_element(locator)
        element.click()
        logger.info(f"点击元素: {locator}")

    def input_text(self, locator, text):
        """输入文本"""
        element = self.find_element(locator)
        element.clear()
        element.send_keys(text)
        logger.info(f"在元素 {locator} 中输入: {text}")

    def get_text(self, locator):
        """获取元素文本"""
        element = self.find_element(locator)
        text = element.text
        logger.info(f"获取元素 {locator} 文本: {text}")
        return text

然后,实现一个具体的页面,例如 page_objects/login_page.py

from selenium.webdriver.common.by import By
from page_objects.base_page import BasePage

class LoginPage(BasePage):
    # 页面元素定位器(推荐使用元组形式)
    USERNAME_INPUT = (By.ID, 'username')
    PASSWORD_INPUT = (By.ID, 'password')
    LOGIN_BUTTON = (By.XPATH, '//button[@type="submit"]')
    ERROR_MSG = (By.CLASS_NAME, 'error-message')
    SUCCESS_MSG = (By.ID, 'welcome')

    def __init__(self, driver):
        super().__init__(driver)
        self.driver = driver

    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_error_message(self):
        """获取错误信息"""
        return self.get_text(self.ERROR_MSG)

    def get_welcome_message(self):
        """获取登录成功欢迎信息"""
        return self.get_text(self.SUCCESS_MSG)

这样,在测试脚本中,我们只需要关心业务逻辑: login_page.login(“user”, “pass”) ,而不用关心元素是怎么找到的、怎么点击的。如果页面元素ID变了,我们只需要修改 LoginPage 类中的定位器即可。

5. 测试用例编写与组织

有了前面的基础设施,现在可以愉快地编写测试用例了。我们将遵循pytest的规则来组织用例。

5.1 编写第一个API测试用例

test_cases/test_api/test_user_api.py 中,我们编写一个简单的用户接口测试:

import pytest
import requests
from configs.env_config import config
from utils.logger import logger

class TestUserAPI:
    """用户相关API测试"""
    base_url = config.get('base_url')
    api_prefix = '/api/v1'

    def test_get_user_info_success(self):
        """测试成功获取用户信息"""
        user_id = 1
        url = f"{self.base_url}{self.api_prefix}/users/{user_id}"
        logger.info(f"请求URL: {url}")

        response = requests.get(url)
        logger.info(f"响应状态码: {response.status_code}")
        logger.info(f"响应体: {response.text}")

        # 断言
        assert response.status_code == 200
        json_data = response.json()
        assert json_data['id'] == user_id
        assert 'username' in json_data
        assert 'email' in json_data

    @pytest.mark.parametrize("user_id, expected_status", [
        (999, 404),  # 不存在的用户
        ('abc', 400), # 无效的用户ID格式
    ])
    def test_get_user_info_failure(self, user_id, expected_status):
        """测试获取用户信息失败场景(参数化)"""
        url = f"{self.base_url}{self.api_prefix}/users/{user_id}"
        response = requests.get(url)
        assert response.status_code == expected_status

这个例子展示了基本的API测试结构:准备请求、发送请求、记录日志、断言响应。 @pytest.mark.parametrize 装饰器让我们能用多组数据运行同一个测试函数,高效覆盖多种边界情况。

5.2 编写第一个UI测试用例

UI测试需要管理浏览器的生命周期。我们使用pytest的fixture来实现。首先,在 test_cases/conftest.py 中定义全局fixture:

import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from configs.env_config import config
from utils.logger import logger

@pytest.fixture(scope="function")  # 每个测试函数执行一次
def driver():
    """初始化WebDriver"""
    options = webdriver.ChromeOptions()
    if config.getboolean('headless'):
        options.add_argument('--headless')  # 无头模式
    options.add_argument('--disable-gpu')
    options.add_argument('--no-sandbox')
    options.add_argument('--window-size=1920,1080')

    # 使用webdriver-manager自动管理驱动
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    driver.implicitly_wait(config.getint('timeout'))  # 隐式等待
    logger.info("Chrome浏览器已启动")

    yield driver  # 将driver对象提供给测试用例

    # 测试结束后执行清理
    driver.quit()
    logger.info("Chrome浏览器已关闭")

@pytest.fixture
def login_page(driver):
    """提供登录页面对象"""
    from page_objects.login_page import LoginPage
    return LoginPage(driver)

然后,在 test_cases/test_ui/test_login.py 中编写UI测试:

import pytest
import allure  # 可选,用于生成更漂亮的Allure报告步骤
from data.data_loader import DataLoader
from configs.env_config import config

# 加载测试数据
test_data = DataLoader.load_yaml('test_data.yaml')
positive_cases = test_data['login_test']['positive']
negative_cases = test_data['login_test']['negative']

class TestLogin:
    """登录功能测试"""

    @pytest.mark.parametrize("case_data", positive_cases)
    def test_login_success(self, driver, login_page, case_data):
        """正向用例:登录成功"""
        username = case_data['username']
        password = case_data['password']
        expected = case_data['expected']

        # 访问登录页
        driver.get(config.get('base_url') + '/login')

        # 执行登录操作
        login_page.login(username, password)

        # 断言
        welcome_text = login_page.get_welcome_message()
        assert expected in welcome_text
        # 也可以断言URL跳转等

    @pytest.mark.parametrize("case_data", negative_cases)
    def test_login_failure(self, driver, login_page, case_data):
        """反向用例:登录失败"""
        username = case_data['username']
        password = case_data['password']
        expected = case_data['expected']

        driver.get(config.get('base_url') + '/login')
        login_page.login(username, password)

        error_text = login_page.get_error_message()
        assert expected in error_text

注意,测试用例函数名要以 test_ 开头,pytest才能自动发现它们。我们通过 driver login_page 这两个fixture,轻松获得了浏览器驱动和页面对象。数据驱动让用例非常简洁。

6. 高级特性与框架优化

基础框架跑通后,我们可以加入一些“高级”特性,让它更健壮、更高效。

6.1 失败重试与截图功能

测试环境不稳定导致用例偶发失败很常见。我们可以用 pytest-rerunfailures 插件来实现失败自动重试。在 pytest.ini 配置文件中添加:

[pytest]
addopts = --html=reports/report.html --self-contained-html --reruns 2 --reruns-delay 1
testpaths = test_cases
python_files = test_*.py
python_classes = Test*
python_functions = test_*

--reruns 2 表示失败后重试2次, --reruns-delay 1 表示每次重试间隔1秒。

同时,我们需要增强框架,在测试失败时自动截图。修改 conftest.py 中的 driver fixture,或者创建一个新的fixture:

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """
    获取测试用例执行结果的钩子函数,用于实现失败截图。
    """
    outcome = yield
    report = outcome.get_result()
    # 只关注用例调用(call)阶段,且是失败或错误的情况
    if report.when == "call" and report.failed:
        # 尝试从fixture中获取driver对象
        for name, fixture_value in item.funcargs.items():
            if name == "driver" and hasattr(fixture_value, 'get_screenshot_as_file'):
                # 确保reports/assets目录存在
                import os
                screenshot_dir = os.path.join(os.path.dirname(__file__), '..', 'reports', 'assets')
                os.makedirs(screenshot_dir, exist_ok=True)
                # 生成截图文件名
                from datetime import datetime
                timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                screenshot_path = os.path.join(screenshot_dir, f"{item.name}_{timestamp}.png")
                fixture_value.save_screenshot(screenshot_path)
                # 将截图路径添加到html报告中(需要pytest-html支持)
                if hasattr(report, 'extra'):
                    from pytest_html import extras
                    report.extra.append(extras.image(screenshot_path))
                logger.error(f"测试失败,截图已保存至: {screenshot_path}")
                break

这段代码是一个pytest钩子,它会在每个测试用例执行后检查结果。如果失败了,它会遍历测试用例的参数,找到名为 driver 的fixture(即我们的浏览器对象),然后调用其截图方法保存图片,并尝试将图片链接添加到HTML报告中。

6.2 测试报告生成

我们使用 pytest-html 插件生成报告。配置已经在 pytest.ini addopts 中指定了 --html=reports/report.html 。运行测试后,会在 reports 目录下生成一个独立的HTML文件。报告里会包含测试概述、通过/失败/跳过的用例列表、每个用例的执行时长,以及我们上面添加的失败截图链接。

如果你需要更强大、更美观的报告,可以考虑使用 Allure 。它需要额外安装Java和Allure命令行工具,但生成的报告支持趋势图、用例分类、步骤详情等高级功能,非常适合大型项目。集成Allure需要安装 pytest-allure 插件,并在运行命令中添加 --alluredir=./allure-results 参数。

6.3 测试用例标记与筛选

当用例成百上千时,我们可能只想运行某一类用例。pytest的mark功能可以轻松实现。 首先,在 pytest.ini 中注册自定义标记,避免警告:

[pytest]
# ... 其他配置 ...
markers =
    smoke: 冒烟测试用例
    regression: 回归测试用例
    slow: 执行较慢的用例

然后,在测试用例上打标记:

import pytest

class TestLogin:
    @pytest.mark.smoke  # 标记为冒烟测试
    def test_login_with_admin(self):
        pass

    @pytest.mark.regression
    @pytest.mark.slow
    def test_login_with_multiple_roles(self):
        pass

运行测试时,可以按标记筛选:

# 只运行冒烟测试
pytest -m smoke
# 运行除了慢用例外的所有用例
pytest -m "not slow"
# 运行回归测试或冒烟测试
pytest -m "regression or smoke"

7. 框架运行、集成与维护

框架搭建完毕,最后一步是让它能方便地运行,并融入团队的工作流。

7.1 运行测试与常用命令

在项目根目录下,我们可以通过不同的pytest命令来运行测试:

# 1. 运行所有测试
pytest

# 2. 运行指定目录下的测试
pytest test_cases/test_ui/

# 3. 运行指定文件中的测试
pytest test_cases/test_ui/test_login.py

# 4. 运行指定类中的测试
pytest test_cases/test_ui/test_login.py::TestLogin

# 5. 运行指定测试方法
pytest test_cases/test_ui/test_login.py::TestLogin::test_login_success

# 6. 使用标记筛选运行
pytest -m smoke
pytest -m "not slow"

# 7. 输出详细日志
pytest -v
# 输出更详细的日志,包括print语句
pytest -v -s

# 8. 生成JUnit XML格式报告,便于CI工具(如Jenkins)解析
pytest --junitxml=reports/junit.xml

为了方便团队使用,可以在项目根目录创建一个 run_tests.py 脚本或 Makefile 来封装常用命令。

7.2 集成到CI/CD流水线

自动化测试只有集成到CI/CD中,每次代码提交后自动运行,才能真正发挥作用。以GitLab CI为例,可以在项目根目录创建 .gitlab-ci.yml 文件:

stages:
  - test

ui-automation-test:
  stage: test
  image: python:3.9-slim  # 使用带有Python的Docker镜像
  before_script:
    - apt-get update && apt-get install -y wget unzip chromium chromium-driver  # 安装浏览器和驱动
    - pip install -r requirements.txt
  script:
    - export CONFIG_ENV=test  # 设置测试环境
    - pytest test_cases/ --html=reports/report.html --self-contained-html --junitxml=reports/junit.xml
  artifacts:
    when: always
    paths:
      - reports/
    expire_in: 1 week
  only:
    - merge_requests  # 仅在合并请求时触发
    - main           # 或在推送到主分支时触发

这样,每当有代码合并请求时,GitLab会自动在一个干净的环境中安装依赖,运行所有测试,并生成报告。测试结果(通过/失败)会直接显示在合并请求页面上。

7.3 框架维护与最佳实践

框架不是一劳永逸的,需要持续维护。这里分享几个关键实践:

  1. 代码审查 :所有测试代码(包括页面对象、工具类、测试用例)的提交都应进行代码审查,确保符合框架规范。
  2. 定期重构 :随着业务变化,及时重构页面对象和工具方法,删除重复代码,优化定位策略(如优先使用ID、CSS Selector,谨慎使用XPath)。
  3. 用例稳定性监控 :关注CI流水线中用例的通过率。对经常“假失败”(flaky)的用例要重点分析,是环境问题、脚本问题还是应用本身不稳定,并加以修复或标记。
  4. 数据与环境隔离 :测试数据必须是独立的,不能影响线上或其他测试环境。使用测试专用的账号、数据,并在用例执行前后做好清理(如删除测试创建的数据)。
  5. 文档与培训 :维护一个简单的框架使用Wiki,记录目录结构、编写规范、常用命令。对新成员进行框架使用的培训。

踩过不少坑之后,我最大的体会是: 不要追求一步到位的大而全框架 。先从解决团队当前最痛的1-2个问题开始(比如先把冒烟测试自动化),采用本文这种渐进式的方式搭建,让框架随着项目和团队一起成长。初期结构清晰、易于扩展比功能繁多更重要。当团队所有人都能遵循同一套规范,顺畅地添加和维护用例时,这个框架的价值就真正体现出来了。

更多推荐