UI_Testing 项目详细文档

UI_Testing 项目详细文档

本文档详细讲解 UI_Testing 项目的每一个文件、每一个知识点、实现原理、使用方法、最佳实践等
⚠️ 注意: 本文档中的 URL、用户名、密码等均为示例占位符,实际使用时请替换为真实值。


📑 目录

  1. 项目概述
  2. 项目结构
  3. 配置文件详解
  4. 工具类详解
  5. 页面对象详解
  6. 测试文件详解
  7. 运行脚本详解
  8. 断言方法详解
  9. 文件关联关系
  10. 最佳实践
  11. 常见问题

项目概述

技术栈

  • 测试框架: pytest 7.4.3
  • 浏览器自动化: Playwright 1.40.0
  • 测试报告: pytest-html, Allure
  • 验证码识别: ddddocr
  • 数据驱动: JSON 文件
  • 日志系统: Python logging

设计模式

  • Page Object Model (POM): 页面对象模式
  • Fixture 模式: pytest fixtures 管理测试资源
  • 数据驱动测试 (DDT): 使用 JSON 文件驱动测试
  • 单例模式: Logger 日志管理器

项目结构

复制代码
UI_Testing/
├── config/                          # 配置文件目录
│   ├── config.json                  # 主配置文件
│   ├── test_data.json               # 测试数据文件
│   └── test_data_ddt.json           # 数据驱动测试数据
├── pages/                           # 页面对象目录
│   ├── __init__.py
│   ├── login_page.py                # 登录页面对象
│   └── goods_detail_page.py         # 商品详情页面对象
├── tests/                           # 测试用例目录
│   ├── __init__.py
│   ├── conftest.py                  # pytest 配置和 fixtures
│   ├── test_login.py                # 登录功能测试
│   ├── test_login_ddt.py            # 登录功能数据驱动测试
│   └── test_goods_detail.py         # 商品详情功能测试
├── utils/                           # 工具类目录
│   ├── __init__.py
│   ├── browser.py                   # 浏览器管理器
│   ├── captcha.py                   # 验证码识别工具
│   ├── logger.py                    # 日志系统
│   ├── ddt.py                       # 数据驱动测试工具
│   └── helpers.py                   # 辅助函数
├── screenshots/                     # 截图目录(自动生成)
├── videos/                          # 视频目录(自动生成)
├── logs/                            # 日志目录(自动生成)
├── reports/                         # 测试报告目录(自动生成)
├── allure-results/                  # Allure 报告数据(自动生成)
├── pytest.ini                       # pytest 配置文件
├── requirements.txt                 # 依赖包列表
├── run_all_tests.py                 # 运行测试脚本(Python)
├── run_all_tests.bat                # 运行测试脚本(Windows)
├── run_all_tests.ps1                # 运行测试脚本(PowerShell)
├── 生成Allure报告.bat               # 生成 Allure 报告脚本
├── 生成Allure报告.ps1               # 生成 Allure 报告脚本
└── .gitignore                       # Git 忽略文件配置

配置文件详解

1. config/config.json - 主配置文件

作用: 存储项目的全局配置信息,包括浏览器配置、URL配置、超时设置等。

文件位置 : config/config.json

完整内容:

json 复制代码
{
  "base_url": "http://your-api-server.com:8081",
  "login_url": "http://your-api-server.com:8081/home#/login",
  "browser": "chromium",
  "headless": false,
  "timeout": 30000,
  "screenshot": true,
  "video": false,
  "captcha": {
    "auto_recognize": true,
    "method": "ddddocr",
    "retry_times": 3
  },
  "viewport": {
    "width": 1920,
    "height": 1080
  },
  "browsers": {
    "chromium": {
      "channel": null
    },
    "firefox": {},
    "webkit": {}
  }
}

配置项详解:

配置项 类型 说明 使用位置
base_url string 测试环境的基础URL utils/helpers.py -> get_base_url()
login_url string 登录页面的完整URL utils/helpers.py -> get_login_url()
browser string 默认浏览器类型(chromium/firefox/webkit) utils/browser.py -> BrowserManager.start_browser()
headless boolean 是否无头模式运行(true=后台运行,false=显示浏览器) utils/browser.py -> BrowserManager.start_browser()
timeout number 默认超时时间(毫秒) utils/browser.py -> BrowserManager.start_browser()
screenshot boolean 是否启用截图功能 测试失败时自动截图
video boolean 是否录制测试视频 utils/browser.py -> BrowserManager.start_browser()
captcha.auto_recognize boolean 是否自动识别验证码 pages/login_page.py
captcha.method string 验证码识别方法(ddddocr/tesseract) utils/captcha.py
captcha.retry_times number 验证码识别失败时的重试次数 pages/login_page.py
viewport.width number 浏览器窗口宽度(像素) utils/browser.py -> BrowserManager.start_browser()
viewport.height number 浏览器窗口高度(像素) utils/browser.py -> BrowserManager.start_browser()
browsers.chromium.channel string/null Chromium 浏览器通道(chrome/msedge/null) utils/browser.py -> BrowserManager.start_browser()

如何读取配置:

python 复制代码
# 方式1: 使用 helpers.py 中的函数
from utils.helpers import get_base_url, get_login_url

base_url = get_base_url()  # 返回: "http://your-api-server.com:8081"
login_url = get_login_url()  # 返回: "http://your-api-server.com:8081/home#/login"

# 方式2: 直接读取 JSON 文件
import json
import os

config_path = os.path.join(os.path.dirname(__file__), 'config', 'config.json')
with open(config_path, 'r', encoding='utf-8') as f:
    config = json.load(f)
    browser_type = config['browser']  # 返回: "chromium"
    timeout = config['timeout']  # 返回: 30000

如何修改配置:

  1. 直接编辑 config/config.json 文件
  2. 修改后重新运行测试即可生效
  3. 建议将配置文件加入版本控制,但敏感信息(如密码)应使用环境变量

2. config/test_data.json - 测试数据文件

作用: 存储测试用例使用的测试数据,如用户名、密码等。

文件位置 : config/test_data.json

完整内容:

json 复制代码
{
  "login": {
    "valid_user": {
      "username": "your_username",
      "password": "your_password"
    },
    "invalid_user": {
      "username": "invalid_user",
      "password": "wrong_password"
    },
    "empty_username": {
      "username": "",
      "password": "test_password"
    },
    "empty_password": {
      "username": "test_user",
      "password": ""
    }
  }
}

数据结构说明:

  • 使用嵌套的 JSON 结构组织测试数据
  • 第一层:功能模块(如 login
  • 第二层:测试场景(如 valid_user, invalid_user
  • 第三层:数据字段(如 username, password

如何读取数据:

python 复制代码
# 方式1: 使用 helpers.py 中的 get_test_data() 函数(推荐)
from utils.helpers import get_test_data

username = get_test_data('login.valid_user.username')  # 返回: "your_username"
password = get_test_data('login.valid_user.password')  # 返回: "your_password"

# 方式2: 使用 load_test_data() 函数获取整个数据对象
from utils.helpers import load_test_data

data = load_test_data()
username = data['login']['valid_user']['username']
password = data['login']['valid_user']['password']

点号路径说明:

  • get_test_data('login.valid_user.username') 使用点号(.)分隔路径
  • 等同于访问 data['login']['valid_user']['username']
  • 优势:路径简洁、易读、不易出错

使用场景:

python 复制代码
# 在测试文件中使用
def test_login_with_valid_credentials(self, page, test_base_url):
    from utils.helpers import get_test_data
    
    login_page = LoginPage(page)
    login_page.navigate_to_login(test_base_url)
    
    # 从测试数据文件读取用户名和密码
    username = get_test_data('login.valid_user.username')
    password = get_test_data('login.valid_user.password')
    
    # 执行登录
    login_success = login_page.login(username, password, auto_captcha=True)
    
    # 断言
    assert login_success, "登录应该成功"

最佳实践:

  1. ✅ 不要在代码中硬编码测试数据
  2. ✅ 使用描述性的键名(如 valid_user 而不是 user1
  3. ✅ 敏感信息(如密码)建议使用环境变量
  4. ✅ 不同环境使用不同的测试数据文件
  5. ❌ 不要将真实的生产环境密码提交到版本控制

3. config/test_data_ddt.json - 数据驱动测试数据

作用: 存储数据驱动测试(DDT)使用的测试数据,用于参数化测试。

文件位置 : config/test_data_ddt.json

完整内容:

json 复制代码
{
  "test_data": [
    {
      "username": "your_username",
      "password": "your_password",
      "expected_result": "success",
      "description": "有效用户登录"
    },
    {
      "username": "invalid_user",
      "password": "wrong_password",
      "expected_result": "failure",
      "description": "无效用户登录"
    },
    {
      "username": "",
      "password": "test_password",
      "expected_result": "failure",
      "description": "空用户名登录"
    },
    {
      "username": "test_user",
      "password": "",
      "expected_result": "failure",
      "description": "空密码登录"
    }
  ]
}

数据结构说明:

  • 顶层有一个 test_data 数组
  • 数组中每个对象代表一个测试用例的数据
  • 每个对象包含:输入数据(username, password)、期望结果(expected_result)、描述(description

如何读取数据:

python 复制代码
# 使用 ddt.py 中的 parametrize_from_json() 函数
from utils.ddt import parametrize_from_json

# 加载数据并转换为 pytest.mark.parametrize 格式
ddt_data = parametrize_from_json('config/test_data_ddt.json')

# 返回格式:
# {
#   'argnames': 'username,password,expected_result,description',
#   'argvalues': [
#     ('your_username', 'your_password', 'success', '有效用户登录'),
#     ('invalid_user', 'wrong_password', 'failure', '无效用户登录'),
#     ...
#   ],
#   'ids': ['username=your_username_password=your_password', ...]
# }

在测试中使用:

python 复制代码
# 在 test_login_ddt.py 中的使用方式
from utils.ddt import parametrize_from_json
import pytest

# 加载数据
ddt_data = parametrize_from_json('config/test_data_ddt.json')

@pytest.mark.parametrize(
    ddt_data['argnames'],  # 参数名: 'username,password,expected_result,description'
    ddt_data['argvalues'],  # 参数值列表
    ids=ddt_data['ids']  # 测试ID列表
)
def test_login_with_ddt(self, page, test_base_url, username, password, 
                        expected_result, description):
    # username, password, expected_result, description 会自动从 ddt_data 中获取
    login_page = LoginPage(page)
    login_page.navigate_to_login(test_base_url)
    
    login_success = login_page.login(username, password, auto_captcha=True)
    
    if expected_result == "success":
        assert login_success, f"登录应该成功: {description}"
    else:
        assert not login_success, f"登录应该失败: {description}"

数据驱动测试的优势:

  1. 减少代码重复: 一个测试函数可以测试多个场景
  2. 易于扩展: 添加新测试数据只需在 JSON 文件中添加一条记录
  3. 清晰的数据分离: 测试逻辑和测试数据分离
  4. 便于维护: 修改测试数据不需要修改代码
  5. 自动生成测试用例: pytest 会根据数据自动生成多个测试用例

运行结果示例:

复制代码
tests/test_login_ddt.py::TestLoginDDT::test_login_with_ddt[username=your_username_password=your_password] PASSED
tests/test_login_ddt.py::TestLoginDDT::test_login_with_ddt[username=invalid_user_password=wrong_password] PASSED
tests/test_login_ddt.py::TestLoginDDT::test_login_with_ddt[username=_password=test_password] SKIPPED
tests/test_login_ddt.py::TestLoginDDT::test_login_with_ddt[username=test_user_password=] SKIPPED

4. pytest.ini - pytest 配置文件

作用: 配置 pytest 的行为,包括测试文件匹配规则、标记定义、日志配置、报告配置等。

文件位置 : pytest.ini(项目根目录)

完整内容:

ini 复制代码
[pytest]
# pytest 配置文件

# 测试文件匹配模式
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*

# 标记定义
markers =
    smoke: 冒烟测试
    regression: 回归测试
    login: 登录相关测试
    ui: UI测试
    ddt: 数据驱动测试

# 日志配置
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S

# 显示详细输出
addopts = 
    -v
    --tb=short
    --strict-markers
    --capture=no
    --html=reports/report.html
    --self-contained-html
    --alluredir=allure-results

# 超时配置(秒)
timeout = 300
timeout_method = thread

配置项详解:

测试文件匹配模式
配置项 说明
testpaths tests pytest 会在 tests 目录下查找测试文件
python_files test_*.py 测试文件必须以 test_ 开头,如 test_login.py
python_classes Test* 测试类必须以 Test 开头,如 TestLogin
python_functions test_* 测试函数必须以 test_ 开头,如 test_login()

示例:

  • tests/test_login.py 中的 class TestLogin 中的 def test_login() 会被识别为测试
  • tests/login.py 中的 class LoginTest 中的 def login() 不会被识别为测试
标记定义(Markers)

作用: 定义测试标记,用于分类和过滤测试用例。

已定义的标记:

标记 说明 使用示例
@pytest.mark.smoke 冒烟测试 @pytest.mark.smoke
@pytest.mark.regression 回归测试 @pytest.mark.regression
@pytest.mark.login 登录相关测试 @pytest.mark.login
@pytest.mark.ui UI测试 @pytest.mark.ui
@pytest.mark.ddt 数据驱动测试 @pytest.mark.ddt

在测试中使用:

python 复制代码
@pytest.mark.login
@pytest.mark.smoke
class TestLogin:
    def test_login_with_valid_credentials(self, page, test_base_url):
        # 这个测试同时带有 login 和 smoke 标记
        pass

运行特定标记的测试:

bash 复制代码
# 只运行冒烟测试
pytest -m smoke

# 只运行登录相关测试
pytest -m login

# 运行登录或UI测试(OR逻辑)
pytest -m "login or ui"

# 运行登录且冒烟测试(AND逻辑)
pytest -m "login and smoke"

# 运行除了登录测试外的所有测试(NOT逻辑)
pytest -m "not login"

添加新标记:

  1. pytest.inimarkers 部分添加新标记
  2. 在测试代码中使用 @pytest.mark.新标记名
  3. 运行 pytest --strict-markers 检查标记是否正确
日志配置
配置项 说明
log_cli true 在控制台显示日志
log_cli_level INFO 控制台日志级别(DEBUG/INFO/WARNING/ERROR)
log_cli_format %(asctime)s [%(levelname)8s] %(name)s: %(message)s 日志格式
log_cli_date_format %Y-%m-%d %H:%M:%S 日期时间格式

日志格式说明:

  • %(asctime)s: 时间戳
  • %(levelname)8s: 日志级别(右对齐,8个字符宽度)
  • %(name)s: logger 名称
  • %(message)s: 日志消息

输出示例:

复制代码
2024-12-30 14:30:25 [    INFO] tests.test_login: 开始测试登录功能
2024-12-30 14:30:26 [   ERROR] tests.test_login: 登录失败
显示详细输出(addopts)
选项 说明
-v 详细输出模式
--tb=short 错误追踪格式(short=简短,long=详细)
--strict-markers 严格检查标记(未定义的标记会报错)
--capture=no 不捕获输出(显示 print 语句)
--html=reports/report.html 生成 HTML 报告
--self-contained-html 生成独立的 HTML 报告(包含所有资源)
--alluredir=allure-results Allure 报告数据目录

addopts 的作用:

  • addopts 中的选项会自动应用到所有 pytest 命令
  • 相当于每次运行 pytest 时自动添加这些选项
  • 例如:pytest 等同于 pytest -v --tb=short --strict-markers ...
超时配置
配置项 说明
timeout 300 单个测试用例的最大运行时间(秒)
timeout_method thread 超时检测方法(thread/signal)

超时机制:

  • 如果测试用例运行时间超过 300 秒,会自动终止并标记为失败
  • thread 方法:使用线程检测超时(Windows 兼容性好)
  • signal 方法:使用信号检测超时(Unix/Linux 系统)

工具类详解

1. utils/browser.py - 浏览器管理器

作用: 管理 Playwright 浏览器的生命周期,包括启动、创建上下文、创建页面、关闭等。

文件位置 : utils/browser.py

核心类 : BrowserManager

类结构:

python 复制代码
class BrowserManager:
    def __init__(self, config_file: str = 'config/config.json')
    def _load_config(self, config_file: str) -> dict
    def start_browser(self, browser_type: str = None)
    def get_page(self) -> Page
    def close_browser(self)
    def take_screenshot(self, filename: str = None) -> str
初始化方法 __init__()

作用: 初始化浏览器管理器,加载配置,初始化实例变量。

代码解析:

python 复制代码
def __init__(self, config_file: str = 'config/config.json'):
    """
    初始化浏览器管理器
    
    Args:
        config_file: 配置文件路径
    """
    self.config = self._load_config(config_file)  # 加载配置文件
    self.playwright: Optional[Playwright] = None  # Playwright 实例
    self.browser: Optional[Browser] = None  # Browser 实例
    self.context: Optional[BrowserContext] = None  # BrowserContext 实例
    self.page: Optional[Page] = None  # Page 实例

知识点:

  1. 类型提示 : Optional[Playwright] 表示该变量可以是 Playwright 类型或 None
  2. 配置文件加载 : 调用 _load_config() 方法加载配置
  3. 延迟初始化 : 浏览器相关对象在 start_browser() 时才创建
配置加载方法 _load_config()

作用: 加载 JSON 配置文件。

代码解析:

python 复制代码
def _load_config(self, config_file: str) -> dict:
    """加载配置"""
    config_path = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
        config_file
    )
    with open(config_path, 'r', encoding='utf-8') as f:
        return json.load(f)

知识点:

  1. 路径解析 :
    • __file__: 当前文件的路径(utils/browser.py
    • os.path.abspath(__file__): 绝对路径
    • os.path.dirname(): 获取目录路径
    • 两次 os.path.dirname()utils/browser.py 回到项目根目录
  2. 文件编码 : 使用 encoding='utf-8' 确保中文正确读取
  3. 上下文管理器 : 使用 with open() 自动关闭文件
启动浏览器方法 start_browser()

作用: 启动 Playwright 浏览器,创建上下文和页面。

代码解析:

python 复制代码
def start_browser(self, browser_type: str = None):
    """
    启动浏览器
    
    Args:
        browser_type: 浏览器类型 (chromium/firefox/webkit)
    """
    from playwright.sync_api import sync_playwright
    
    # 1. 启动 Playwright
    self.playwright = sync_playwright().start()
    
    # 2. 确定浏览器类型
    browser_type = browser_type or self.config.get('browser', 'chromium')
    browser_config = self.config.get('browsers', {}).get(browser_type, {})
    
    # 3. 准备启动参数
    launch_options = {
        'headless': self.config.get('headless', False)
    }
    
    # 4. 根据浏览器类型启动
    if browser_type == 'chromium':
        channel = browser_config.get('channel')
        if channel:
            try:
                launch_options['channel'] = channel
                self.browser = self.playwright.chromium.launch(**launch_options)
            except Exception as e:
                # 如果 channel 启动失败,使用默认方式
                launch_options.pop('channel', None)
                self.browser = self.playwright.chromium.launch(**launch_options)
        else:
            self.browser = self.playwright.chromium.launch(**launch_options)
    elif browser_type == 'firefox':
        self.browser = self.playwright.firefox.launch(**launch_options)
    elif browser_type == 'webkit':
        self.browser = self.playwright.webkit.launch(**launch_options)
    
    # 5. 创建上下文
    viewport = self.config.get('viewport', {'width': 1920, 'height': 1080})
    self.context = self.browser.new_context(
        viewport={
            'width': viewport.get('width', 1920),
            'height': viewport.get('height', 1080)
        },
        record_video_dir='videos/' if self.config.get('video', False) else None
    )
    
    # 6. 创建页面
    self.page = self.context.new_page()
    self.page.set_default_timeout(self.config.get('timeout', 30000))

知识点:

  1. sync_playwright(): Playwright 的同步 API 入口
  2. headless 模式 :
    • headless=True: 后台运行,不显示浏览器窗口(适合 CI/CD)
    • headless=False: 显示浏览器窗口(适合调试)
  3. channel 参数 :
    • channel='chrome': 使用系统安装的 Chrome 浏览器
    • channel='msedge': 使用系统安装的 Edge 浏览器
    • channel=None: 使用 Playwright 自带的 Chromium
  4. 异常处理: 如果使用 channel 启动失败,自动降级到默认方式
  5. 上下文(Context) :
    • 隔离的浏览器上下文,每个上下文有独立的 cookies、localStorage 等
    • 可以创建多个上下文来模拟多个用户
  6. 页面(Page) :
    • 浏览器标签页,用于执行操作
    • 一个上下文可以创建多个页面
  7. 默认超时: 设置页面操作的默认超时时间(毫秒)
获取页面方法 get_page()

作用: 获取当前页面对象。

代码解析:

python 复制代码
def get_page(self) -> Page:
    """获取当前页面"""
    if not self.page:
        raise RuntimeError("浏览器未启动,请先调用 start_browser()")
    return self.page

知识点:

  1. 检查状态: 确保浏览器已启动
  2. 返回类型 : -> Page 表示返回 Playwright 的 Page 对象
关闭浏览器方法 close_browser()

作用: 关闭浏览器和相关资源。

代码解析:

python 复制代码
def close_browser(self):
    """关闭浏览器"""
    if self.page:
        self.page.close()
    if self.context:
        self.context.close()
    if self.browser:
        self.browser.close()
    if self.playwright:
        self.playwright.stop()

知识点:

  1. 关闭顺序: 按照页面 -> 上下文 -> 浏览器 -> Playwright 的顺序关闭
  2. 检查存在 : 使用 if 检查每个对象是否存在再关闭
  3. 资源清理: 确保所有资源都被正确释放
截图方法 take_screenshot()

作用: 对当前页面进行截图。

代码解析:

python 复制代码
def take_screenshot(self, filename: str = None) -> str:
    """
    截图
    
    Args:
        filename: 文件名,默认使用时间戳
    
    Returns:
        str: 截图文件路径
    """
    if not self.page:
        raise RuntimeError("页面未创建")
    
    if not filename:
        from datetime import datetime
        filename = f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
    
    screenshot_dir = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
        'screenshots'
    )
    os.makedirs(screenshot_dir, exist_ok=True)
    
    screenshot_path = os.path.join(screenshot_dir, filename)
    self.page.screenshot(path=screenshot_path, full_page=True)
    return screenshot_path

知识点:

  1. 时间戳文件名: 如果没有指定文件名,使用当前时间戳生成文件名
  2. 目录创建 : os.makedirs(screenshot_dir, exist_ok=True) 创建目录(如果不存在)
  3. 全页截图 : full_page=True 截取整个页面(包括滚动区域)
  4. 返回路径: 返回截图文件的完整路径,方便后续使用

使用示例:

python 复制代码
browser_manager = BrowserManager()
browser_manager.start_browser()

# 截图
screenshot_path = browser_manager.take_screenshot("test.png")
print(f"截图保存到: {screenshot_path}")

# 或者使用默认文件名
screenshot_path = browser_manager.take_screenshot()
# 文件名类似: screenshot_20241230_143025.png

2. utils/captcha.py - 验证码识别工具

作用: 识别验证码图片中的文字,支持 ddddocr 和 tesseract 两种识别方法。

文件位置 : utils/captcha.py

核心类 : CaptchaRecognizer, ManualCaptchaInput

CaptchaRecognizer 类

作用: 自动识别验证码。

类结构:

python 复制代码
class CaptchaRecognizer:
    def __init__(self)
    def recognize(self, image_path: str) -> str
    def _recognize_with_ddddocr(self, image_path: str) -> str
    def _recognize_with_tesseract(self, image_path: str) -> str
初始化方法 __init__()

作用: 初始化验证码识别器,优先使用 ddddocr,如果不可用则尝试 tesseract。

代码解析:

python 复制代码
def __init__(self):
    """初始化识别器"""
    self.ocr = None
    
    # 优先使用 ddddocr(准确率更高)
    if DDDDOCR_AVAILABLE:
        try:
            self.ocr = ddddocr.DdddOcr(show_ad=False)
            self.method = 'ddddocr'
        except Exception as e:
            print(f"初始化 ddddocr 失败: {e}")
            self.ocr = None
    
    # 如果 ddddocr 不可用,尝试使用 pytesseract
    if self.ocr is None and TESSERACT_AVAILABLE:
        try:
            self.method = 'tesseract'
        except Exception as e:
            print(f"初始化 tesseract 失败: {e}")
            self.method = None
    
    if self.ocr is None and self.method is None:
        print("警告: 未安装验证码识别库,将返回空字符串")

知识点:

  1. 库可用性检查 :

    python 复制代码
    try:
        import ddddocr
        DDDDOCR_AVAILABLE = True
    except ImportError:
        DDDDOCR_AVAILABLE = False
  2. 优先级策略: ddddocr 准确率更高,优先使用

  3. 降级策略: 如果 ddddocr 不可用,自动降级到 tesseract

  4. 错误处理: 使用 try-except 捕获初始化错误

识别方法 recognize()

作用: 识别验证码图片。

代码解析:

python 复制代码
def recognize(self, image_path: str) -> str:
    """
    识别验证码
    
    Args:
        image_path: 验证码图片路径
    
    Returns:
        str: 识别出的验证码文本
    """
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"验证码图片不存在: {image_path}")
    
    if self.method == 'ddddocr' and self.ocr:
        return self._recognize_with_ddddocr(image_path)
    elif self.method == 'tesseract':
        return self._recognize_with_tesseract(image_path)
    else:
        print("警告: 验证码识别功能不可用,返回空字符串")
        return ""

知识点:

  1. 文件存在性检查 : 使用 os.path.exists() 检查图片文件是否存在
  2. 方法分发 : 根据 self.method 调用不同的识别方法
  3. 错误处理: 如果识别方法不可用,返回空字符串
ddddocr 识别方法 _recognize_with_ddddocr()

作用: 使用 ddddocr 库识别验证码。

代码解析:

python 复制代码
def _recognize_with_ddddocr(self, image_path: str) -> str:
    """使用 ddddocr 识别"""
    try:
        with open(image_path, 'rb') as f:
            image_bytes = f.read()
        result = self.ocr.classification(image_bytes)
        return result.strip()
    except Exception as e:
        print(f"ddddocr 识别失败: {e}")
        return ""

知识点:

  1. 二进制读取 : 使用 'rb' 模式以二进制方式读取图片
  2. 字节输入: ddddocr 接受字节数据,而不是文件路径
  3. 文本清理 : 使用 strip() 去除首尾空白字符
tesseract 识别方法 _recognize_with_tesseract()

作用: 使用 tesseract 库识别验证码。

代码解析:

python 复制代码
def _recognize_with_tesseract(self, image_path: str) -> str:
    """使用 tesseract 识别"""
    try:
        image = Image.open(image_path)
        # 转换为灰度图
        if image.mode != 'L':
            image = image.convert('L')
        # OCR识别
        text = pytesseract.image_to_string(
            image, 
            config='--psm 7 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
        )
        return text.strip()
    except Exception as e:
        print(f"tesseract 识别失败: {e}")
        return ""

知识点:

  1. PIL/Pillow : 使用 Image.open() 打开图片
  2. 灰度转换: 转换为灰度图('L' 模式)提高识别准确率
  3. PSM 模式 : --psm 7 表示单行文本识别
  4. 字符白名单 : tessedit_char_whitelist 限制识别的字符范围(数字和字母)

使用示例:

python 复制代码
from utils.captcha import CaptchaRecognizer

recognizer = CaptchaRecognizer()
captcha_text = recognizer.recognize('screenshots/captcha.png')
print(f"识别出的验证码: {captcha_text}")

3. utils/logger.py - 日志系统

作用: 提供统一的日志管理功能,支持控制台输出、文件输出、日志轮转等。

文件位置 : utils/logger.py

核心类 : Logger

设计模式: 单例模式

Logger 类

作用: 管理日志配置和输出。

类结构:

python 复制代码
class Logger:
    _instance = None
    _logger = None
    
    def __new__(cls)
    def __init__(self)
    def _setup_logger(self)
    def get_logger(self)
    def debug(self, message: str)
    def info(self, message: str)
    def warning(self, message: str)
    def error(self, message: str, exc_info=False)
    def critical(self, message: str, exc_info=False)
单例模式实现

作用: 确保整个应用只有一个 Logger 实例。

代码解析:

python 复制代码
_instance = None
_logger = None

def __new__(cls):
    if cls._instance is None:
        cls._instance = super(Logger, cls).__new__(cls)
    return cls._instance

知识点:

  1. 单例模式: 确保只有一个实例存在
  2. 类变量 : _instance_logger 是类变量,所有实例共享
  3. __new__ 方法 : 在 __init__ 之前调用,用于控制实例创建
日志配置方法 _setup_logger()

作用: 配置日志格式、输出位置、日志级别等。

代码解析:

python 复制代码
def _setup_logger(self):
    """设置日志配置"""
    # 1. 创建日志目录
    log_dir = Path(__file__).parent.parent / 'logs'
    log_dir.mkdir(exist_ok=True)
    
    # 2. 创建 logger
    self._logger = logging.getLogger('UI_Testing')
    self._logger.setLevel(logging.DEBUG)
    
    # 3. 避免重复添加 handler
    if self._logger.handlers:
        return
    
    # 4. 日志格式
    formatter = logging.Formatter(
        '%(asctime)s [%(levelname)8s] %(name)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    
    # 5. 控制台输出 handler(INFO 级别及以上)
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)
    self._logger.addHandler(console_handler)
    
    # 6. 文件输出 handler(DEBUG 级别及以上)
    log_file = log_dir / f'test_{datetime.now().strftime("%Y%m%d")}.log'
    file_handler = RotatingFileHandler(
        log_file,
        maxBytes=10 * 1024 * 1024,  # 10MB
        backupCount=5,  # 保留5个备份文件
        encoding='utf-8'
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)
    self._logger.addHandler(file_handler)
    
    # 7. 错误日志单独文件
    error_log_file = log_dir / f'error_{datetime.now().strftime("%Y%m%d")}.log'
    error_handler = RotatingFileHandler(
        error_log_file,
        maxBytes=10 * 1024 * 1024,
        backupCount=5,
        encoding='utf-8'
    )
    error_handler.setLevel(logging.ERROR)
    error_handler.setFormatter(formatter)
    self._logger.addHandler(error_handler)

知识点:

  1. Path 对象 : 使用 pathlib.Path 进行路径操作
  2. 日志级别 :
    • DEBUG: 调试信息
    • INFO: 一般信息
    • WARNING: 警告信息
    • ERROR: 错误信息
    • CRITICAL: 严重错误
  3. Handler 类型 :
    • StreamHandler: 输出到控制台
    • RotatingFileHandler: 输出到文件,支持日志轮转
  4. 日志轮转 :
    • maxBytes: 单个日志文件最大大小(10MB)
    • backupCount: 保留的备份文件数量(5个)
    • 当日志文件超过 10MB 时,自动创建新文件,旧文件重命名为 test_20241230.log.1
  5. 日志格式 :
    • %(asctime)s: 时间戳
    • %(levelname)8s: 日志级别(8个字符宽度,右对齐)
    • %(name)s: logger 名称
    • %(message)s: 日志消息
  6. 重复 Handler 检查: 避免重复添加 handler(单例模式需要)
便捷函数

作用: 提供全局的日志函数,方便使用。

代码解析:

python 复制代码
# 创建全局 logger 实例
logger = Logger().get_logger()

# 便捷函数
def get_logger(name: str = None):
    """获取 logger 实例"""
    if name:
        return logging.getLogger(name)
    return logger

def debug(message: str):
    """DEBUG 级别日志"""
    logger.debug(message)

def info(message: str):
    """INFO 级别日志"""
    logger.info(message)

# ... 其他级别

使用示例:

python 复制代码
# 方式1: 使用便捷函数
from utils.logger import info, error, debug

info("这是一条信息日志")
error("这是一条错误日志")
debug("这是一条调试日志")

# 方式2: 使用 get_logger()
from utils.logger import get_logger

logger = get_logger(__name__)
logger.info("这是一条信息日志")
logger.error("这是一条错误日志", exc_info=True)  # exc_info=True 会输出异常堆栈

日志输出示例:

复制代码
控制台输出(INFO 及以上):
2024-12-30 14:30:25 [    INFO] UI_Testing - 这是一条信息日志
2024-12-30 14:30:26 [   ERROR] UI_Testing - 这是一条错误日志

文件输出(logs/test_20241230.log,DEBUG 及以上):
2024-12-30 14:30:25 [   DEBUG] UI_Testing - 这是一条调试日志
2024-12-30 14:30:25 [    INFO] UI_Testing - 这是一条信息日志
2024-12-30 14:30:26 [   ERROR] UI_Testing - 这是一条错误日志

错误日志文件(logs/error_20241230.log,ERROR 及以上):
2024-12-30 14:30:26 [   ERROR] UI_Testing - 这是一条错误日志

4. utils/ddt.py - 数据驱动测试工具

作用: 从 JSON、CSV 等文件加载测试数据,并转换为 pytest 参数化格式。

文件位置 : utils/ddt.py

核心类 : DataDriver

便捷函数 : load_test_data_from_file(), parametrize_from_json()

DataDriver 类

作用: 提供数据驱动的核心功能。

类方法:

python 复制代码
class DataDriver:
    @staticmethod
    def load_json_data(file_path: str) -> List[Dict[str, Any]]
    @staticmethod
    def load_csv_data(file_path: str) -> List[Dict[str, Any]]
    @staticmethod
    def load_test_data(file_path: str) -> List[Dict[str, Any]]
    @staticmethod
    def parametrize_from_file(file_path: str, test_ids: List[str] = None) -> Dict
加载 JSON 数据 load_json_data()

作用: 从 JSON 文件加载测试数据。

代码解析:

python 复制代码
@staticmethod
def load_json_data(file_path: str) -> List[Dict[str, Any]]:
    """
    从 JSON 文件加载测试数据
    
    Args:
        file_path: JSON 文件路径
    
    Returns:
        List[Dict]: 测试数据列表
    """
    abs_path = Path(__file__).parent.parent / file_path
    with open(abs_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    
    # 如果数据是字典,转换为列表
    if isinstance(data, dict):
        # 尝试提取测试数据数组
        if 'test_data' in data:
            return data['test_data']
        elif 'data' in data:
            return data['data']
        else:
            # 将字典转换为列表
            return [data]
    
    return data if isinstance(data, list) else [data]

知识点:

  1. 路径解析 : 使用 Path(__file__).parent.parent 从项目根目录开始
  2. 数据结构适配 : 支持多种 JSON 结构
    • {"test_data": [...]} → 提取 test_data 数组
    • {"data": [...]} → 提取 data 数组
    • {...} → 转换为 [{...}]
    • [...] → 直接返回
参数化转换 parametrize_from_file()

作用: 将测试数据转换为 pytest.mark.parametrize 格式。

代码解析:

python 复制代码
@staticmethod
def parametrize_from_file(file_path: str, test_ids: List[str] = None) -> Dict:
    """
    从文件加载数据并生成参数化测试参数
    
    Args:
        file_path: 测试数据文件路径
        test_ids: 测试ID列表(可选)
    
    Returns:
        Dict: pytest.mark.parametrize 参数字典
    """
    data = DataDriver.load_test_data(file_path)
    
    if not data:
        return
    
    # 获取所有键名
    keys = list(data[0].keys())
    
    # 生成参数值列表
    values = []
    ids = []
    
    for i, item in enumerate(data):
        row_values = tuple(item[key] for key in keys)
        values.append(row_values)
        
        # 生成测试ID
        if test_ids and i < len(test_ids):
            ids.append(test_ids[i])
        else:
            # 自动生成ID
            id_parts = [f"{key}={item[key]}" for key in keys[:2]]  # 只取前两个字段
            ids.append("_".join(id_parts))
    
    # 返回 pytest.mark.parametrize 格式
    return {
        'argnames': ','.join(keys),
        'argvalues': values,
        'ids': ids
    }

知识点:

  1. 数据转换 :
    • [{"username": "user1", "password": "pwd1"}, ...]
    • 转换为 {"argnames": "username,password", "argvalues": [("user1", "pwd1"), ...], "ids": [...]}
  2. 测试ID生成 :
    • 如果没有提供 test_ids,自动生成
    • 格式: username=user1_password=pwd1
    • 只取前两个字段生成ID(避免ID过长)
  3. pytest 参数化格式 :
    • argnames: 参数名称(逗号分隔)
    • argvalues: 参数值列表(每个元素是一个元组)
    • ids: 测试ID列表(可选)

使用示例:

python 复制代码
# JSON 文件 (config/test_data_ddt.json):
{
  "test_data": [
    {"username": "user1", "password": "pwd1", "expected": "success"},
    {"username": "user2", "password": "pwd2", "expected": "failure"}
  ]
}

# 使用方式:
from utils.ddt import parametrize_from_json

ddt_data = parametrize_from_json('config/test_data_ddt.json')
# 返回:
# {
#   'argnames': 'username,password,expected',
#   'argvalues': [('user1', 'pwd1', 'success'), ('user2', 'pwd2', 'failure')],
#   'ids': ['username=user1_password=pwd1', 'username=user2_password=pwd2']
# }

# 在测试中使用:
@pytest.mark.parametrize(
    ddt_data['argnames'],
    ddt_data['argvalues'],
    ids=ddt_data['ids']
)
def test_login(username, password, expected):
    # username, password, expected 会自动从 ddt_data 中获取
    pass

5. utils/helpers.py - 辅助函数

作用: 提供常用的辅助函数,如读取测试数据、获取URL等。

文件位置 : utils/helpers.py

核心函数:

python 复制代码
def load_test_data(data_file: str = 'config/test_data.json') -> Dict[str, Any]
def get_test_data(key_path: str, data_file: str = 'config/test_data.json') -> Any
def wait_for_element(page, selector: str, timeout: int = 30000)
def get_base_url(config_file: str = 'config/config.json') -> str
def get_login_url(config_file: str = 'config/config.json') -> str
load_test_data()

作用: 加载整个测试数据文件。

代码解析:

python 复制代码
def load_test_data(data_file: str = 'config/test_data.json') -> Dict[str, Any]:
    """
    加载测试数据
    
    Args:
        data_file: 测试数据文件路径
    
    Returns:
        dict: 测试数据
    """
    data_path = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
        data_file
    )
    with open(data_path, 'r', encoding='utf-8') as f:
        return json.load(f)

使用示例:

python 复制代码
from utils.helpers import load_test_data

data = load_test_data()
username = data['login']['valid_user']['username']
get_test_data()

作用: 使用点号路径获取测试数据。

代码解析:

python 复制代码
def get_test_data(key_path: str, data_file: str = 'config/test_data.json') -> Any:
    """
    获取测试数据(支持点号路径)
    
    Args:
        key_path: 数据路径,如 'login.valid_user.username'
        data_file: 测试数据文件路径
    
    Returns:
        数据值
    """
    data = load_test_data(data_file)
    keys = key_path.split('.')
    
    value = data
    for key in keys:
        if isinstance(value, dict) and key in value:
            value = value[key]
        else:
            raise KeyError(f"测试数据路径不存在: {key_path}")
    
    return value

知识点:

  1. 点号路径 : 'login.valid_user.username' 等同于 data['login']['valid_user']['username']
  2. 路径解析 : 使用 split('.') 分割路径
  3. 递归访问: 遍历路径中的每个键,逐步深入字典
  4. 错误处理 : 如果路径不存在,抛出 KeyError

使用示例:

python 复制代码
from utils.helpers import get_test_data

# 简单直观
username = get_test_data('login.valid_user.username')  # 返回: "your_username"
password = get_test_data('login.valid_user.password')  # 返回: "your_password"

# 对比传统方式(更繁琐)
data = load_test_data()
username = data['login']['valid_user']['username']
password = data['login']['valid_user']['password']
get_base_url() 和 get_login_url()

作用: 从配置文件获取 URL。

代码解析:

python 复制代码
def get_base_url(config_file: str = 'config/config.json') -> str:
    """获取基础URL"""
    import json
    config_path = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
        config_file
    )
    with open(config_path, 'r', encoding='utf-8') as f:
        config = json.load(f)
    return config.get('base_url', '')

def get_login_url(config_file: str = 'config/config.json') -> str:
    """获取登录URL"""
    # 类似实现
    return config.get('login_url', config.get('base_url', '') + '/home#/login')

使用示例:

python 复制代码
from utils.helpers import get_base_url, get_login_url

base_url = get_base_url()  # 返回: "http://your-api-server.com:8081"
login_url = get_login_url()  # 返回: "http://your-api-server.com:8081/home#/login"

页面对象详解

Page Object Model (POM) 设计模式

什么是 POM?

Page Object Model(页面对象模型)是一种设计模式,用于将页面元素和页面操作封装成类。这样做的好处是:

  1. 代码复用: 页面操作可以在多个测试中复用
  2. 易于维护: 页面元素定位集中在页面对象中,页面变化时只需修改一处
  3. 可读性强: 测试代码更清晰,接近自然语言
  4. 职责分离: 测试逻辑和页面操作分离

POM 的基本原则:

  • 每个页面对应一个页面对象类
  • 页面元素定位(XPath、CSS选择器等)作为类的属性
  • 页面操作(点击、输入、获取文本等)作为类的方法
  • 页面对象类不包含断言(断言在测试代码中)

1. pages/login_page.py - 登录页面对象

作用: 封装登录页面的所有元素和操作。

文件位置 : pages/login_page.py

核心类 : LoginPage

类结构:

python 复制代码
class LoginPage:
    def __init__(self, page: Page)
    def navigate_to_login(self, base_url: str = None)
    def enter_username(self, username: str)
    def enter_password(self, password: str)
    def refresh_captcha(self)
    def get_captcha_image(self) -> str
    def recognize_captcha(self) -> str
    def enter_captcha(self, captcha: str = None)
    def click_login_button(self)
    def login(self, username: str, password: str, captcha: str = None, 
              auto_captcha: bool = True, retry_times: int = 3) -> bool
    def get_error_message(self) -> Optional[str]
    def get_success_message(self) -> Optional[str]
    def is_login_successful(self, wait_time: int = 3000) -> bool
初始化方法 __init__()

作用: 初始化登录页面对象,定义所有页面元素的定位器。

代码解析:

python 复制代码
def __init__(self, page: Page):
    """
    初始化登录页面
    
    Args:
        page: Playwright Page 对象
    """
    self.page = page  # 保存 Page 对象
    self.captcha_recognizer = CaptchaRecognizer()  # 初始化验证码识别器
    
    # 页面元素定位(使用 XPath)
    self.username_input = "//*[@id='van-field-1-input']"  # 用户名输入框
    self.password_input = "//*[@id='van-field-2-input']"  # 密码输入框
    self.captcha_image = "//*[@id='app']/div/div[2]/form/div[3]/div[2]/div/div/div/canvas"  # 验证码图片
    self.captcha_input = "//*[@id='van-field-3-input']"  # 验证码输入框
    self.login_button = "//*[@id='app']/div/div[2]/form/div[4]/button/div/span"  # 登录按钮
    
    # 登录成功后的特征元素
    self.header_title = "//*[@id='app']/div/header/div/span"  # 头部标题"新蜂商城"
    self.new_products_header = "//*[@id='app']/div/div[4]/header"  # "新品上线"标题

知识点:

  1. XPath 定位 : 使用 XPath 表达式定位元素
    • //*[@id='van-field-1-input']: 查找 id 为 van-field-1-input 的元素
    • // 表示从根节点查找,* 表示任意标签名
  2. 元素定位器存储: 将定位器存储在实例变量中,便于维护
  3. 验证码识别器 : 初始化 CaptchaRecognizer 用于识别验证码

作用: 导航到登录页面并等待页面加载完成。

代码解析:

python 复制代码
def navigate_to_login(self, base_url: str = None):
    """
    导航到登录页面
    
    Args:
        base_url: 基础URL,如果为None则使用配置中的URL
    """
    if base_url:
        login_url = f"{base_url}/home#/login"
    else:
            login_url = "http://your-api-server.com:8081/home#/login"
    
    self.page.goto(login_url)  # 导航到登录页面
    self.page.wait_for_load_state('networkidle')  # 等待网络空闲(页面加载完成)
    
    # 等待页面元素加载
    self.page.wait_for_selector(self.username_input, timeout=10000)

知识点:

  1. page.goto(): Playwright 的导航方法,跳转到指定URL
  2. wait_for_load_state() : 等待页面加载状态
    • 'networkidle': 等待网络空闲(所有请求完成)
    • 'domcontentloaded': 等待 DOM 加载完成
    • 'load': 等待页面完全加载
  3. wait_for_selector(): 等待元素出现,确保页面元素已加载
输入用户名和密码

代码解析:

python 复制代码
def enter_username(self, username: str):
    """输入用户名"""
    if self.username_input:
        self.page.fill(self.username_input, username)
    else:
        raise ValueError("用户名输入框定位器未设置,请先配置 xpath")

def enter_password(self, password: str):
    """输入密码"""
    self.page.fill(self.password_input, password)

知识点:

  1. page.fill() : Playwright 的填充方法,用于输入文本
    • 会先清空输入框,然后输入新文本
    • 等同于 click() + type()
验证码识别和输入

代码解析:

python 复制代码
def get_captcha_image(self) -> str:
    """获取验证码图片并保存"""
    # 等待验证码元素出现
    self.page.wait_for_selector(self.captcha_image, timeout=5000)
    
    # 截图保存验证码
    screenshots_dir = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
        'screenshots'
    )
    os.makedirs(screenshots_dir, exist_ok=True)
    
    captcha_path = os.path.join(screenshots_dir, 'captcha.png')
    
    # 截图验证码区域
    captcha_element = self.page.locator(self.captcha_image)
    captcha_element.screenshot(path=captcha_path)
    
    return captcha_path

def recognize_captcha(self) -> str:
    """识别验证码"""
    captcha_path = self.get_captcha_image()
    captcha_text = self.captcha_recognizer.recognize(captcha_path)
    print(f"[验证码] 验证码图片路径: {captcha_path}")
    print(f"[验证码] 识别结果: {captcha_text}")
    return captcha_text

def refresh_captcha(self):
    """刷新验证码(点击验证码图片)"""
    captcha_element = self.page.locator(self.captcha_image)
    captcha_element.click()
    self.page.wait_for_timeout(1000)  # 等待1秒,让验证码刷新

知识点:

  1. element.screenshot(): 对特定元素进行截图
  2. page.locator(): 获取元素定位器对象
  3. element.click(): 点击元素
  4. wait_for_timeout(): 固定时间等待(毫秒)
登录方法 login()

作用: 执行完整的登录流程,包括输入用户名、密码、识别验证码、点击登录按钮,支持自动重试。

代码解析:

python 复制代码
def login(self, username: str, password: str, captcha: str = None, 
          auto_captcha: bool = True, retry_times: int = 3) -> bool:
    """
    执行登录操作(支持验证码刷新和自动识别重试)
    
    Args:
        username: 用户名
        password: 密码
        captcha: 验证码(如果提供则使用,否则自动识别)
        auto_captcha: 是否自动识别验证码,默认True
        retry_times: 验证码错误时的重试次数,默认3次
    
    Returns:
        bool: 登录是否成功
    """
    for attempt in range(1, retry_times + 1):
        # 如果是重试,先刷新验证码
        if attempt > 1:
            self.refresh_captcha()  # 点击验证码图片刷新
            # 清空输入框
            self.page.fill(self.username_input, "")
            self.page.fill(self.password_input, "")
            self.page.fill(self.captcha_input, "")
        
        # 输入用户名和密码
        self.enter_username(username)
        self.enter_password(password)
        
        # 处理验证码
        if captcha is None and auto_captcha:
            captcha = self.recognize_captcha()
        
        if captcha:
            self.enter_captcha(captcha)
        
        # 点击登录按钮
        self.click_login_button()
        
        # 检查是否登录成功
        if self.is_login_successful():
            return True
        else:
            # 如果不是最后一次尝试,继续重试
            if attempt < retry_times:
                captcha = None  # 重置验证码,下次循环会刷新并重新识别
    
    return False

知识点:

  1. 重试机制 : 使用 for 循环实现重试
  2. 验证码刷新: 重试时点击验证码图片刷新
  3. 自动识别 : 使用 CaptchaRecognizer 自动识别验证码
  4. 成功判断 : 使用 is_login_successful() 判断登录是否成功
判断登录成功 is_login_successful()

作用: 判断是否登录成功,使用多种方法综合判断。

代码解析:

python 复制代码
def is_login_successful(self, wait_time: int = 3000) -> bool:
    """
    判断是否登录成功
    
    判断条件(按优先级):
    1. 检查登录成功后的特征元素("新蜂商城"标题和"新品上线"标题)
    2. 检查URL是否变化(不包含login)
    3. 检查是否有错误提示
    """
    self.page.wait_for_timeout(wait_time)  # 等待页面响应
    
    # 优先级1: 检查登录成功后的特征元素
    try:
        header_title_element = self.page.locator(self.header_title)
        if header_title_element.is_visible(timeout=5000):
            header_text = header_title_element.text_content()
            if '新蜂商城' in header_text:
                return True
        
        new_products_element = self.page.locator(self.new_products_header)
        if new_products_element.is_visible(timeout=5000):
            new_products_text = new_products_element.text_content()
            if '新品上线' in new_products_text:
                return True
    except:
        pass
    
    # 优先级2: 检查URL是否变化
    current_url = self.page.url
    if '/login' not in current_url and '#/login' not in current_url:
        return True
    
    return False

知识点:

  1. 多条件判断: 使用多个条件综合判断,提高准确性
  2. 优先级顺序: 按照可靠性排序判断条件
  3. 元素可见性 : 使用 is_visible() 检查元素是否可见
  4. 文本内容 : 使用 text_content() 获取元素文本

2. pages/goods_detail_page.py - 商品详情页面对象

作用: 封装商品详情、购物车、结算、订单等页面的所有元素和操作。

文件位置 : pages/goods_detail_page.py

核心类 : GoodsDetailPage

由于商品详情页面对象的方法较多,这里重点讲解几个核心方法。

验证商品信息

代码解析:

python 复制代码
def verify_goods_name(self, expected_name: str) -> bool:
    """验证商品名称"""
    actual_name = self.get_goods_name()
    if actual_name:
        match = expected_name in actual_name or actual_name in expected_name
        print(f"[商品详情] 商品名称验证: 期望 '{expected_name}', 实际 '{actual_name}', 匹配: {match}")
        return match
    return False

def verify_goods_price(self, expected_price: str) -> bool:
    """验证商品价格"""
    actual_price = self.get_goods_price()
    if actual_price:
        match = expected_price in actual_price or actual_price in expected_price
        print(f"[商品详情] 商品价格验证: 期望 '{expected_price}', 实际 '{actual_price}', 匹配: {match}")
        return match
    return False

知识点:

  1. 部分匹配 : 使用 in 操作符实现部分匹配(支持文本截断)
  2. 双向匹配 : expected_name in actual_name or actual_name in expected_name 支持两种匹配方向
购物车操作

代码解析:

python 复制代码
def click_add_to_cart(self) -> bool:
    """点击加入购物车按钮"""
    try:
        add_to_cart_element = self.page.locator(self.add_to_cart_button)
        add_to_cart_element.wait_for(state='visible', timeout=5000)
        add_to_cart_element.click()
        self.page.wait_for_timeout(2000)  # 等待操作完成
        return True
    except Exception as e:
        print(f"[购物车] 点击加入购物车按钮失败: {e}")
        return False

def verify_goods_in_cart(self, expected_goods_name: str) -> bool:
    """验证购物车中是否包含指定商品"""
    try:
        cart_content_element = self.page.locator(self.cart_content)
        cart_content_element.wait_for(state='visible', timeout=5000)
        
        cart_text = cart_content_element.text_content()
        
        if cart_text and expected_goods_name in cart_text:
            return True
        return False
    except Exception as e:
        print(f"[购物车] 验证购物车内容失败: {e}")
        return False

知识点:

  1. 元素状态等待 : 使用 wait_for(state='visible') 等待元素可见
  2. 文本内容检查 : 使用 text_content() 获取元素文本并检查是否包含期望内容
等待页面加载完成

代码解析:

python 复制代码
def _wait_for_order_page_loaded(self, max_wait_time: int = 10000) -> bool:
    """等待订单页面加载完成(等待"加载中..."消失)"""
    start_time = time.time()
    while (time.time() - start_time) * 1000 < max_wait_time:
        try:
            goods_name_element = self.page.locator(self.order_goods_name)
            goods_name_text = goods_name_element.text_content()
            if goods_name_text and goods_name_text.strip() != "加载中...":
                return True
        except:
            pass
        self.page.wait_for_timeout(500)  # 每500ms检查一次
    return False

知识点:

  1. 轮询检查 : 使用 while 循环轮询检查页面状态
  2. 时间控制 : 使用 time.time() 计算已等待时间
  3. 条件等待: 等待特定条件("加载中..."消失)而不是固定时间

页面对象最佳实践

  1. 一个页面一个类: 每个页面对应一个页面对象类
  2. 元素定位器集中管理: 所有元素定位器作为类属性
  3. 方法命名清晰 : 使用动词命名方法(如 click_login_button, enter_username
  4. 方法粒度适中: 每个方法完成一个具体操作
  5. 返回值明确 : 操作方法返回 bool 表示成功/失败,查询方法返回实际值
  6. 异常处理: 关键操作使用 try-except 捕获异常
  7. 日志输出: 关键操作输出日志,便于调试
  8. 不要包含断言: 断言应该在测试代码中,不在页面对象中

测试文件详解

1. tests/conftest.py - pytest 配置和 Fixtures

作用: 定义 pytest fixtures,包括浏览器管理、页面创建、登录状态等。

文件位置 : tests/conftest.py

核心 Fixtures:

python 复制代码
@pytest.fixture(scope="session")
def browser_manager()

@pytest.fixture(scope="function")
def page(browser_manager, request)

@pytest.fixture(scope="session")
def test_base_url()

@pytest.fixture(scope="session")
def logged_in_page(browser_manager, test_base_url)
browser_manager fixture

作用: 创建并管理浏览器实例(session 级别,整个测试会话只创建一次)。

代码解析:

python 复制代码
@pytest.fixture(scope="session")
def browser_manager():
    """
    浏览器管理器 fixture(session 级别)
    """
    manager = BrowserManager()
    manager.start_browser()
    yield manager  # 返回浏览器管理器
    manager.close_browser()  # 测试结束后关闭浏览器

知识点:

  1. Fixture 作用域 :
    • scope="session": 整个测试会话只执行一次(所有测试共享)
    • scope="function": 每个测试函数执行一次(默认)
    • scope="class": 每个测试类执行一次
    • scope="module": 每个测试模块执行一次
  2. yield 关键字 :
    • yield 之前的代码在 fixture 启动时执行
    • yield 之后的代码在 fixture 清理时执行
    • yield 返回的值作为 fixture 的返回值
page fixture

作用: 创建页面对象(function 级别,每个测试创建一个新页面)。

代码解析:

python 复制代码
@pytest.fixture(scope="function")
def page(browser_manager, request):
    """
    页面 fixture(function 级别)
    
    Args:
        browser_manager: 浏览器管理器(依赖注入)
        request: pytest request 对象
    """
    page = browser_manager.get_page()
    yield page
    
    # 测试失败时截图
    if request.node.rep_call.failed if hasattr(request.node, 'rep_call') else False:
        test_name = request.node.name
        browser_manager.take_screenshot(f"failure_{test_name}.png")

知识点:

  1. 依赖注入 : browser_manager 作为参数,pytest 会自动注入
  2. request 对象: pytest 提供的请求对象,包含测试信息
  3. 失败截图: 使用 pytest hook 检测测试失败并截图
logged_in_page fixture

作用: 创建已登录的页面(session 级别,整个测试会话只登录一次)。

代码解析:

python 复制代码
@pytest.fixture(scope="session")
def logged_in_page(browser_manager, test_base_url):
    """
    已登录的页面 fixture(session 级别)
    整个测试会话只登录一次,所有测试共享登录状态
    """
    from pages.login_page import LoginPage
    from utils.helpers import get_test_data
    
    page = browser_manager.get_page()
    login_page = LoginPage(page)
    
    # 只登录一次
    login_page.navigate_to_login(test_base_url)
    
    username = get_test_data('login.valid_user.username')
    password = get_test_data('login.valid_user.password')
    
    if username and password:
        login_success = login_page.login(
            username,
            password,
            auto_captcha=True
        )
        if login_success:
            print("[Session Fixture] ✅ 会话登录成功")
    
    yield page  # 返回已登录的页面

知识点:

  1. Session 级别登录: 整个测试会话只登录一次,提高测试效率
  2. 登录状态共享 : 所有使用 logged_in_page fixture 的测试共享登录状态
  3. 延迟导入: 在 fixture 内部导入,避免循环导入

使用示例:

python 复制代码
def test_something(logged_in_page):
    # logged_in_page 已经是登录后的页面,不需要再次登录
    page = logged_in_page
    # 执行测试...

2. tests/test_login.py - 登录功能测试

作用: 测试登录功能的各个场景。

文件位置 : tests/test_login.py

测试类 : TestLogin

测试用例:

python 复制代码
@pytest.mark.login
@pytest.mark.smoke
class TestLogin:
    def test_login_with_valid_credentials(self, page, test_base_url)
    def test_login_with_invalid_credentials(self, page, test_base_url)
    def test_login_with_empty_username(self, page, test_base_url)
    def test_login_with_empty_password(self, page, test_base_url)
有效凭据登录测试

代码解析:

python 复制代码
def test_login_with_valid_credentials(self, page, test_base_url):
    """
    测试使用有效凭据登录(自动识别验证码)
    
    验证点:
    - 能够成功输入用户名和密码
    - 能够自动识别并输入验证码
    - 点击登录按钮后能够成功登录
    """
    login_page = LoginPage(page)
    login_page.navigate_to_login(test_base_url)
    
    # 从测试数据获取有效用户信息
    username = get_test_data('login.valid_user.username')
    password = get_test_data('login.valid_user.password')
    
    # 检查测试数据是否已配置
    if not username or not password:
        pytest.skip("测试账号未配置")
    
    # 执行登录(自动识别验证码,失败时自动刷新重试)
    login_success = login_page.login(
        username, 
        password, 
        auto_captcha=True
    )
    
    # 验证登录成功
    assert login_success, f"登录应该成功。当前URL: {page.url}"

知识点:

  1. 测试函数命名 : 以 test_ 开头
  2. 测试类命名 : 以 Test 开头
  3. 测试标记 : 使用 @pytest.mark.login@pytest.mark.smoke 标记测试
  4. 断言 : 使用 assert 进行断言
  5. 跳过测试 : 使用 pytest.skip() 跳过测试

3. tests/test_login_ddt.py - 数据驱动登录测试

作用: 使用数据驱动方式测试登录功能。

文件位置 : tests/test_login_ddt.py

测试类 : TestLoginDDT

代码解析:

python 复制代码
from utils.ddt import parametrize_from_json
import pytest

# 从 JSON 文件加载测试数据
ddt_data = parametrize_from_json('config/test_data_ddt.json')

@pytest.mark.login
@pytest.mark.ddt
class TestLoginDDT:
    @pytest.mark.parametrize(
        ddt_data['argnames'],
        ddt_data['argvalues'],
        ids=ddt_data['ids']
    )
    def test_login_with_ddt(self, page, test_base_url, username, password, 
                            expected_result, description):
        """数据驱动测试:使用不同数据测试登录功能"""
        login_page = LoginPage(page)
        login_page.navigate_to_login(test_base_url)
        
        # 跳过空用户名或空密码的测试
        if not username or not password:
            pytest.skip(f"跳过测试: {description}")
        
        # 执行登录
        login_success = login_page.login(
            username,
            password,
            auto_captcha=True
        )
        
        # 验证结果
        if expected_result == "success":
            assert login_success, f"登录应该成功: {description}"
        else:
            assert not login_success, f"登录应该失败: {description}"

知识点:

  1. 数据驱动 : 使用 @pytest.mark.parametrize 实现数据驱动
  2. 参数化 : 测试函数参数会自动从 argvalues 中获取
  3. 测试ID : ids 参数用于生成测试ID,便于识别

运行结果:

复制代码
tests/test_login_ddt.py::TestLoginDDT::test_login_with_ddt[username=your_username_password=your_password] PASSED
tests/test_login_ddt.py::TestLoginDDT::test_login_with_ddt[username=invalid_user_password=wrong_password] PASSED
tests/test_login_ddt.py::TestLoginDDT::test_login_with_ddt[username=_password=test_password] SKIPPED
tests/test_login_ddt.py::TestLoginDDT::test_login_with_ddt[username=test_user_password=] SKIPPED

4. tests/test_goods_detail.py - 商品详情功能测试

作用: 测试商品详情、购物车、结算、订单等完整流程。

文件位置 : tests/test_goods_detail.py

测试类 : TestGoodsDetail

代码解析:

python 复制代码
@pytest.mark.ui
@pytest.mark.smoke
class TestGoodsDetail:
    def test_view_goods_detail_after_login(self, logged_in_page):
        """
        测试登录后查看商品详情
        
        测试步骤:
        1. 使用已登录的页面(session 级别,只登录一次)
        2. 点击商品项
        3. 验证商品详情页面显示正确的商品名称和价格
        4. 加入购物车
        5. 验证购物车
        6. 结算
        7. 生成订单
        8. 支付
        9. 验证订单
        """
        page = logged_in_page  # 使用已登录的页面
        
        # 步骤1: 点击商品项
        goods_item_xpath = "//*[@id='app']/div/div[4]/div/div[1]/div/div[1]"
        goods_detail_page = GoodsDetailPage(page)
        goods_detail_page.click_goods_item(goods_item_xpath)
        
        # 步骤2: 验证商品详情
        expected_name = "HUAWEI Mate 30 Pro 双4000万徕卡电影四摄"
        assert goods_detail_page.verify_goods_name(expected_name), \
            f"商品名称应该为 '{expected_name}'"
        
        expected_price = "¥5399"
        assert goods_detail_page.verify_goods_price(expected_price), \
            f"商品价格应该为 '{expected_price}'"
        
        # 步骤3: 加入购物车
        assert goods_detail_page.click_add_to_cart(), "应该成功点击加入购物车按钮"
        assert goods_detail_page.click_cart_button(), "应该成功点击购物车按钮"
        
        # 步骤4: 验证购物车
        expected_goods_name = "HUAWEI Mate 30 Pro 双4000万徕卡电"
        assert goods_detail_page.verify_goods_in_cart(expected_goods_name), \
            f"购物车中应该包含商品: {expected_goods_name}"
        
        # 步骤5: 结算
        cart_total_amount = goods_detail_page.get_cart_total_amount()
        assert goods_detail_page.click_checkout_button(), "应该成功点击结算按钮"
        assert goods_detail_page.verify_checkout_amount_label("商品金额"), \
            "结算页面应该显示'商品金额'标签"
        assert goods_detail_page.verify_checkout_amount(f"¥{cart_total_amount}"), \
            f"结算页面金额应该为 '¥{cart_total_amount}'"
        
        # 步骤6: 生成订单和支付
        assert goods_detail_page.click_create_order_button(), "应该成功点击生成订单按钮"
        assert goods_detail_page.click_alipay_button(), "应该成功点击支付宝支付按钮"
        
        # 步骤7: 验证订单
        expected_order_goods_name = "HUAWEI Mate 30 Pro 双4000万徕卡电"
        assert goods_detail_page.verify_order_goods_name(expected_order_goods_name), \
            f"订单页面应该包含商品: {expected_order_goods_name}"
        assert goods_detail_page.verify_order_tab_all("全部"), \
            "订单页面应该显示'全部'标签"

知识点:

  1. 完整流程测试: 测试从登录到订单的完整流程
  2. 使用 logged_in_page: 使用 session 级别的登录 fixture,避免重复登录
  3. 步骤清晰: 测试步骤清晰,易于理解和维护
  4. 断言详细: 每个断言都有清晰的错误信息

断言方法详解

pytest 断言

pytest 使用 Python 的内置 assert 语句进行断言。

基本语法:

python 复制代码
assert condition, "错误信息"

如果 conditionFalse,pytest 会:

  • 抛出 AssertionError 异常
  • 显示 "错误信息"(如果提供)
  • 标记测试为失败

常见断言模式

1. 布尔值断言
python 复制代码
# 断言为 True
assert login_success, "登录应该成功"

# 断言为 False
assert not login_success, "登录应该失败"
2. 相等断言
python 复制代码
# 断言相等
assert actual_value == expected_value, f"期望 {expected_value},实际 {actual_value}"

# 断言不相等
assert actual_value != expected_value
3. 包含断言
python 复制代码
# 断言包含
assert "新蜂商城" in page_content, "页面应该包含'新蜂商城'"

# 断言不包含
assert "错误" not in page_content
4. None 断言
python 复制代码
# 断言不为 None
assert element is not None, "元素应该存在"

# 断言为 None
assert error_message is None, "不应该有错误信息"
5. 类型断言
python 复制代码
# 断言类型
assert isinstance(value, str), f"值应该是字符串类型,实际是 {type(value)}"

断言最佳实践

  1. 提供清晰的错误信息:

    python 复制代码
    # 好
    assert login_success, f"登录应该成功。当前URL: {page.url}"
    
    # 不好
    assert login_success
  2. 使用描述性变量名:

    python 复制代码
    # 好
    expected_name = "HUAWEI Mate 30 Pro"
    actual_name = goods_detail_page.get_goods_name()
    assert actual_name == expected_name, f"商品名称应该为 '{expected_name}'"
  3. 一个断言测试一个条件:

    python 复制代码
    # 好
    assert goods_detail_page.verify_goods_name(expected_name)
    assert goods_detail_page.verify_goods_price(expected_price)
    
    # 不好
    assert goods_detail_page.verify_goods_name(expected_name) and \
           goods_detail_page.verify_goods_price(expected_price)
  4. 使用辅助方法:

    python 复制代码
    # 好(在页面对象中定义辅助方法)
    def verify_goods_name(self, expected_name: str) -> bool:
        actual_name = self.get_goods_name()
        return expected_name in actual_name
    
    # 在测试中使用
    assert goods_detail_page.verify_goods_name(expected_name)

文件关联关系

依赖关系图

复制代码
tests/
├── conftest.py
│   ├── 依赖 → utils/browser.py (BrowserManager)
│   ├── 依赖 → utils/helpers.py (get_base_url, get_login_url)
│   └── 依赖 → pages/login_page.py (LoginPage)
│
├── test_login.py
│   ├── 依赖 → pages/login_page.py (LoginPage)
│   └── 依赖 → utils/helpers.py (get_test_data)
│
├── test_login_ddt.py
│   ├── 依赖 → pages/login_page.py (LoginPage)
│   ├── 依赖 → utils/ddt.py (parametrize_from_json)
│   └── 依赖 → utils/logger.py (get_logger)
│
└── test_goods_detail.py
    ├── 依赖 → pages/login_page.py (LoginPage)
    ├── 依赖 → pages/goods_detail_page.py (GoodsDetailPage)
    └── 依赖 → utils/helpers.py (get_test_data)

pages/
├── login_page.py
│   └── 依赖 → utils/captcha.py (CaptchaRecognizer)
│
└── goods_detail_page.py
    └── 无外部依赖

utils/
├── browser.py
│   └── 依赖 → config/config.json
│
├── captcha.py
│   └── 无外部依赖(仅依赖第三方库)
│
├── logger.py
│   └── 无外部依赖(仅依赖标准库)
│
├── ddt.py
│   └── 无外部依赖(仅依赖标准库)
│
└── helpers.py
    ├── 依赖 → config/config.json
    └── 依赖 → config/test_data.json

数据流

复制代码
配置文件 (config/)
    ↓
工具类 (utils/)
    ↓
页面对象 (pages/)
    ↓
测试用例 (tests/)
    ↓
测试报告 (reports/)

调用链示例

登录测试调用链:

复制代码
test_login.py::test_login_with_valid_credentials()
    ↓
LoginPage(page)
    ↓
LoginPage.login(username, password)
    ↓
LoginPage.recognize_captcha()
    ↓
CaptchaRecognizer.recognize(image_path)
    ↓
ddddocr.DdddOcr.classification(image_bytes)

商品详情测试调用链:

复制代码
test_goods_detail.py::test_view_goods_detail_after_login()
    ↓
logged_in_page fixture (conftest.py)
    ↓
BrowserManager.start_browser()
    ↓
LoginPage.login() (登录)
    ↓
GoodsDetailPage(page)
    ↓
GoodsDetailPage.click_goods_item()
    ↓
GoodsDetailPage.verify_goods_name()

最佳实践

1. 代码组织

  • 分层架构: 按照 pages、tests、utils、config 分层
  • 单一职责: 每个类/函数只负责一件事
  • 命名规范: 使用清晰的命名,符合 Python 规范
  • 注释文档: 关键函数和方法添加文档字符串

2. 测试编写

  • 测试独立性: 每个测试应该独立,不依赖其他测试
  • 测试可重复: 测试应该可以重复运行,结果一致
  • 测试清晰: 测试代码应该清晰易懂
  • 测试完整: 覆盖主要功能场景

3. 错误处理

  • 异常捕获: 关键操作使用 try-except 捕获异常
  • 错误信息: 提供清晰的错误信息
  • 日志记录: 记录关键操作和错误

4. 配置管理

  • 配置分离: 将配置数据从代码中分离
  • 环境变量: 敏感信息使用环境变量
  • 配置验证: 启动时验证配置是否完整

5. 性能优化

  • Session 级别 Fixture: 对于耗时的操作(如登录),使用 session 级别 fixture
  • 合理等待: 使用显式等待而不是固定时间等待
  • 资源清理: 及时清理不需要的资源

常见问题

1. 测试失败时如何调试?

方法1: 查看日志

bash 复制代码
# 查看测试日志
cat logs/test_20241230.log

# 查看错误日志
cat logs/error_20241230.log

方法2: 查看截图

测试失败时会自动截图,查看 screenshots/ 目录。

方法3: 使用 headless=False

config/config.json 中设置 "headless": false,可以看到浏览器操作过程。

方法4: 使用 pytest 的调试选项

bash 复制代码
# 显示详细输出
pytest -v -s

# 显示失败时的详细信息
pytest --tb=long

# 遇到第一个失败时停止
pytest -x

2. 验证码识别失败怎么办?

原因:

  • 验证码图片质量差
  • 验证码识别库未正确安装
  • 验证码格式特殊

解决方案:

  1. 检查验证码图片 : 查看 screenshots/captcha.png,确认图片是否清晰
  2. 安装识别库 : pip install ddddocr
  3. 增加重试次数 : 在 config/config.json 中增加 captcha.retry_times
  4. 手动识别: 如果自动识别失败,可以手动查看图片并输入

3. 元素定位失败怎么办?

原因:

  • 元素定位器错误
  • 页面加载未完成
  • 元素被其他元素遮挡
  • 页面结构变化

解决方案:

  1. 检查定位器: 使用浏览器开发者工具检查元素定位器是否正确
  2. 增加等待时间 : 使用 wait_for_selector()wait_for_timeout()
  3. 使用更稳定的定位器: 优先使用 id、name 等稳定属性
  4. 检查页面状态: 确保页面已完全加载

4. 如何运行特定测试?

方法1: 运行特定文件

bash 复制代码
pytest tests/test_login.py

方法2: 运行特定测试类

bash 复制代码
pytest tests/test_login.py::TestLogin

方法3: 运行特定测试函数

bash 复制代码
pytest tests/test_login.py::TestLogin::test_login_with_valid_credentials

方法4: 运行特定标记的测试

bash 复制代码
pytest -m smoke  # 运行冒烟测试
pytest -m login  # 运行登录相关测试

5. 如何生成测试报告?

HTML 报告:

bash 复制代码
# 运行测试(自动生成 HTML 报告)
pytest

# 报告位置: reports/report.html

Allure 报告:

bash 复制代码
# 运行测试(生成 Allure 数据)
pytest

# 生成 Allure 报告
allure serve allure-results

6. 如何并行运行测试?

安装 pytest-xdist:

bash 复制代码
pip install pytest-xdist

运行:

bash 复制代码
# 使用 4 个进程并行运行
pytest -n 4

# 自动检测 CPU 核心数
pytest -n auto

注意: 并行运行测试时,某些测试可能会冲突(如共享登录状态),需要谨慎使用。


总结

本文档详细讲解了 UI_Testing 项目的:

  1. 项目结构: 目录组织和文件说明
  2. 配置文件: 配置项详解和使用方法
  3. 工具类: 浏览器管理、验证码识别、日志系统、数据驱动等
  4. 页面对象: Page Object Model 模式实现
  5. 测试文件: 测试用例编写和 fixtures 使用
  6. 断言方法: pytest 断言的使用和最佳实践
  7. 文件关联: 依赖关系和调用链
  8. 最佳实践: 代码组织、测试编写、错误处理等
  9. 常见问题: 调试方法、问题解决方案

下一步:

  • 阅读代码并结合本文档理解实现细节
  • 根据实际需求修改配置和测试用例
  • 扩展页面对象和测试用例
  • 参考最佳实践优化代码

参考资源:


文档版本 : 1.0
最后更新 : 2024-12-30
维护者: UI_Testing 团队

相关推荐
每天吃饭的羊9 小时前
媒体查询
开发语言·前端·javascript
北海有初拥9 小时前
Python基础语法万字详解
java·开发语言·python
阿里嘎多学长10 小时前
2026-01-02 GitHub 热点项目精选
开发语言·程序员·github·代码托管
XiaoYu200210 小时前
第8章 Three.js入门
前端·javascript·three.js
天远云服10 小时前
Go语言高并发实战:集成天远手机号码归属地核验API打造高性能风控中台
大数据·开发语言·后端·golang
这个一个非常哈10 小时前
element之,自定义form的label
前端·javascript·vue.js
2501_9418771310 小时前
在法兰克福企业级场景中落地零信任安全架构的系统设计与工程实践分享
开发语言·php
李瑞丰_liruifengv10 小时前
Claude Agent SDK 最简玩法:几行代码配合 Markdown 轻松搭建 Agent
javascript·人工智能·程序员
bobringtheboys10 小时前
[el-tag]使用多个el-tag,自动判断内容是否超出
前端·javascript·vue.js
leiming610 小时前
c++ QT 开发第二天,用ui按钮点亮实体led
开发语言·qt·ui