目录
- Web页面UI自动化测试完全指南:8步通用测试框架
-
- 一、框架概述
-
- [1.1 什么是8步通用测试框架?](#1.1 什么是8步通用测试框架?)
- [1.2 为什么是这8步?](#1.2 为什么是这8步?)
- 二、环境准备
-
- [2.1 项目结构(以仅测试登录页面为例)](#2.1 项目结构(以仅测试登录页面为例))
- [2.2 依赖安装](#2.2 依赖安装)
- [2.3 配置文件](#2.3 配置文件)
- 三、基础代码
-
- [3.1 基础页面类](#3.1 基础页面类)
- [3.2 登录页面类](#3.2 登录页面类)
- 四、8步测试详解与代码
-
- [4.1 第1步:元素存在性测试](#4.1 第1步:元素存在性测试)
- [4.2 第2步:正常路径测试](#4.2 第2步:正常路径测试)
- [4.3 第3步:异常路径测试](#4.3 第3步:异常路径测试)
- [4.4 第4步:边界值测试](#4.4 第4步:边界值测试)
- [4.5 第5步:页面跳转测试](#4.5 第5步:页面跳转测试)
- [4.6 第6步:交互体验测试](#4.6 第6步:交互体验测试)
- [4.7 第7步:UI样式测试](#4.7 第7步:UI样式测试)
- [4.8 第8步:兼容性测试](#4.8 第8步:兼容性测试)
- 五、Pytest配置
-
- [5.1 conftest.py](#5.1 conftest.py)
- [5.2 pytest.ini](#5.2 pytest.ini)
- 六、运行测试
-
- [6.1 运行命令](#6.1 运行命令)
- [6.2 测试结果](#6.2 测试结果)
- 七、总结
-
- [7.1 8步框架快速记忆](#7.1 8步框架快速记忆)
- [7.2 框架适用性](#7.2 框架适用性)
- [7.3 核心原则](#7.3 核心原则)
Web页面UI自动化测试完全指南:8步通用测试框架
一套适用于任何Web页面的测试方法论,包含测试要点、代码示例和最佳实践
一、框架概述
1.1 什么是8步通用测试框架?
这是一个系统化的Web页面测试方法论,无论你测试的是登录页、购物车、商品详情还是个人中心,都可以按照这8个步骤来设计测试用例。
| 步骤 | 测试类型 | 核心问题 |
|---|---|---|
| 第1步 | 元素存在性 | 页面上的东西都在吗? |
| 第2步 | 正常路径 | 正确操作能成功吗? |
| 第3步 | 异常路径 | 错误操作有提示吗? |
| 第4步 | 边界值 | 极端输入能处理吗? |
| 第5步 | 页面跳转 | 链接点得对、跳得对吗? |
| 第6步 | 交互体验 | 键盘、鼠标操作顺滑吗? |
| 第7步 | UI样式 | 颜色、字体、布局对吗? |
| 第8步 | 兼容性 | 不同浏览器/分辨率正常吗? |
1.2 为什么是这8步?
用户使用页面的完整路径:
1. 看到页面(元素存在)
2. 输入信息(正常操作)
3. 可能输错(异常处理)
4. 极端情况(边界值)
5. 点击链接(页面跳转)
6. 键盘鼠标(交互体验)
7. 视觉感受(UI样式)
8. 不同设备(兼容性)
这8步覆盖了用户使用页面的所有场景!
二、环境准备
2.1 项目结构(以仅测试登录页面为例)
shopping_ui_test/
├── config/
│ ├── config.yaml # 环境配置
│ └── test_data.yaml # 测试数据
├── pages/
│ ├── base_page.py # 基础页面类
│ └── login_page.py # 登录页面类
├── testcases/
│ └── test_login.py # 测试用例
├── utils/
│ ├── driver_factory.py # 浏览器驱动管理
│ ├── data_loader.py # 数据加载
│ └── logger.py # 日志
├── conftest.py # pytest配置
├── pytest.ini # pytest配置
├── requirements.txt # 依赖
├── screenshots/ # 截图目录
└── reports/ # 报告目录
2.2 依赖安装
bash
pip install pytest selenium webdriver-manager pyyaml allure-pytest
2.3 配置文件
config/config.yaml
yaml
environment: "test"
url:
base_url: "https://example.com"
login: "/login.html"
browser:
name: "chrome"
headless: false
timeout:
implicit_wait: 10
explicit_wait: 15
config/test_data.yaml
yaml
valid_user:
username: "admin"
password: "123456"
invalid_users:
- username: "admin"
password: "wrong"
expected: "密码错误"
- username: "nonexist"
password: "123456"
expected: "账号不存在"
三、基础代码
3.1 基础页面类
python
# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException
import os
from datetime import datetime
class BasePage:
"""所有页面类的基类,封装公共方法"""
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
# ========== 元素定位 ==========
def find_element(self, locator, timeout=10):
"""等待并查找单个元素"""
wait = WebDriverWait(self.driver, timeout)
return wait.until(EC.presence_of_element_located(locator))
def find_elements(self, locator, timeout=10):
"""等待并查找多个元素"""
wait = WebDriverWait(self.driver, timeout)
return wait.until(EC.presence_of_all_elements_located(locator))
def is_element_present(self, locator, timeout=2):
"""判断元素是否存在"""
try:
self.find_element(locator, timeout)
return True
except TimeoutException:
return False
def is_element_visible(self, locator, timeout=5):
"""判断元素是否可见"""
try:
wait = WebDriverWait(self.driver, timeout)
wait.until(EC.visibility_of_element_located(locator))
return True
except TimeoutException:
return False
# ========== 元素操作 ==========
def click(self, locator, timeout=10):
"""点击元素"""
wait = WebDriverWait(self.driver, timeout)
element = wait.until(EC.element_to_be_clickable(locator))
element.click()
def input_text(self, locator, text, timeout=10, clear_first=True):
"""输入文本"""
element = self.find_element(locator, timeout)
if clear_first:
element.clear()
element.send_keys(text)
def get_text(self, locator, timeout=10):
"""获取元素文本"""
return self.find_element(locator, timeout).text
def get_attribute(self, locator, attribute, timeout=10):
"""获取元素属性"""
return self.find_element(locator, timeout).get_attribute(attribute)
# ========== 页面信息 ==========
def get_current_url(self):
"""获取当前URL"""
return self.driver.current_url
def get_page_title(self):
"""获取页面标题"""
return self.driver.title
def refresh(self):
"""刷新页面"""
self.driver.refresh()
# ========== 等待 ==========
def wait_for_url_contains(self, text, timeout=10):
"""等待URL包含指定文本"""
wait = WebDriverWait(self.driver, timeout)
wait.until(EC.url_contains(text))
def wait_for_url_to_be(self, url, timeout=10):
"""等待URL完全匹配"""
wait = WebDriverWait(self.driver, timeout)
wait.until(EC.url_to_be(url))
# ========== 截图 ==========
def take_screenshot(self, name=None):
"""截图并保存"""
screenshot_dir = "screenshots"
if not os.path.exists(screenshot_dir):
os.makedirs(screenshot_dir)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{name}_{timestamp}.png" if name else f"{timestamp}.png"
filepath = os.path.join(screenshot_dir, filename)
self.driver.save_screenshot(filepath)
print(f"截图已保存: {filepath}")
return filepath
# ========== CSS属性获取 ==========
def get_css_color(self, locator, property_name):
"""获取元素的CSS颜色值"""
element = self.find_element(locator)
return element.value_of_css_property(property_name)
def get_element_size(self, locator):
"""获取元素尺寸"""
element = self.find_element(locator)
return element.size
def get_element_location(self, locator):
"""获取元素位置"""
element = self.find_element(locator)
return element.location
3.2 登录页面类
python
# pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from .base_page import BasePage
class LoginPage(BasePage):
"""登录页面对象"""
# ========== 元素定位器 ==========
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "loginBtn")
REGISTER_LINK = (By.LINK_TEXT, "注册")
FORGOT_PASSWORD_LINK = (By.LINK_TEXT, "找回密码")
ERROR_MSG = (By.CLASS_NAME, "error")
REMEMBER_CHECKBOX = (By.ID, "remember")
# ========== 页面操作方法 ==========
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 login_with_enter(self, username, password):
"""使用Enter键登录"""
self.input_text(self.USERNAME_INPUT, username)
password_input = self.find_element(self.PASSWORD_INPUT)
password_input.send_keys(password)
password_input.send_keys(Keys.ENTER)
def get_error_message(self):
"""获取错误提示"""
return self.get_text(self.ERROR_MSG)
def is_login_success(self):
"""判断是否登录成功"""
return "index" in self.get_current_url() or "home" in self.get_current_url()
def click_register_link(self):
"""点击注册链接"""
self.click(self.REGISTER_LINK)
def click_forgot_password_link(self):
"""点击找回密码链接"""
self.click(self.FORGOT_PASSWORD_LINK)
def check_remember_me(self):
"""勾选记住密码"""
self.click(self.REMEMBER_CHECKBOX)
# ========== 元素存在性判断 ==========
def has_username_input(self):
return self.is_element_present(self.USERNAME_INPUT)
def has_password_input(self):
return self.is_element_present(self.PASSWORD_INPUT)
def has_login_button(self):
return self.is_element_present(self.LOGIN_BUTTON)
# ========== 交互操作 ==========
def tab_to_password(self):
"""从用户名框按Tab键到密码框"""
username_input = self.find_element(self.USERNAME_INPUT)
username_input.send_keys(Keys.TAB)
def is_password_focused(self):
"""判断密码框是否获得焦点"""
return self.driver.switch_to.active_element == self.find_element(self.PASSWORD_INPUT)
# ========== UI样式验证 ==========
def get_login_button_color(self):
"""获取登录按钮背景色"""
return self.get_css_color(self.LOGIN_BUTTON, "background-color")
def is_password_masked(self):
"""判断密码框是否为掩码模式"""
password_input = self.find_element(self.PASSWORD_INPUT)
return password_input.get_attribute("type") == "password"
def get_username_input_width(self):
"""获取用户名框宽度"""
return self.get_element_size(self.USERNAME_INPUT)['width']
def get_password_input_width(self):
"""获取密码框宽度"""
return self.get_element_size(self.PASSWORD_INPUT)['width']
def get_input_spacing(self):
"""获取两个输入框之间的间距"""
username_input = self.find_element(self.USERNAME_INPUT)
password_input = self.find_element(self.PASSWORD_INPUT)
username_bottom = username_input.location['y'] + username_input.size['height']
password_top = password_input.location['y']
return password_top - username_bottom
四、8步测试详解与代码
4.1 第1步:元素存在性测试
测什么: 页面上用户必须用到的元素是否存在
python
# testcases/test_login.py
import pytest
from pages.login_page import LoginPage
class TestLogin:
@pytest.fixture(autouse=True)
def open_page(self, driver, base_url):
driver.get(base_url + "/login.html")
self.login_page = LoginPage(driver)
# ========== 第1步:元素存在性测试 ==========
def test_username_input_exists(self):
"""用户名输入框存在"""
assert self.login_page.has_username_input(), "用户名输入框不存在"
def test_password_input_exists(self):
"""密码输入框存在"""
assert self.login_page.has_password_input(), "密码输入框不存在"
def test_login_button_exists(self):
"""登录按钮存在"""
assert self.login_page.has_login_button(), "登录按钮不存在"
def test_register_link_exists(self):
"""注册链接存在"""
assert self.login_page.is_element_present(self.login_page.REGISTER_LINK), "注册链接不存在"
def test_forgot_password_link_exists(self):
"""找回密码链接存在"""
assert self.login_page.is_element_present(self.login_page.FORGOT_PASSWORD_LINK), "找回密码链接不存在"
4.2 第2步:正常路径测试
测什么: 正确操作是否能成功完成功能
python
# ========== 第2步:正常路径测试 ==========
def test_login_success(self, valid_user):
"""正确用户名+正确密码 → 登录成功"""
self.login_page.login(valid_user["username"], valid_user["password"])
# 验证跳转到首页
self.login_page.wait_for_url_contains("index")
assert self.login_page.is_login_success(), "登录成功但未跳转到首页"
4.3 第3步:异常路径测试
测什么: 错误操作是否有正确的提示信息
python
# ========== 第3步:异常路径测试 ==========
@pytest.mark.parametrize("username,password,expected", [
("admin", "wrong", "密码错误"),
("nonexist", "123456", "账号不存在"),
])
def test_login_fail(self, username, password, expected):
"""错误信息 → 显示正确提示"""
self.login_page.login(username, password)
error_msg = self.login_page.get_error_message()
assert expected in error_msg, f'期望包含"{expected}",实际: "{error_msg}"'
4.4 第4步:边界值测试
测什么: 极端输入(空值、超长、特殊字符)能否正确处理
python
# ========== 第4步:边界值测试 ==========
@pytest.mark.parametrize("username,password,expected", [
("", "123456", "请输入用户名"),
("admin", "", "请输入密码"),
("", "", "请输入用户名"),
("a" * 100, "123456", "用户名不能超过"),
("<script>alert(1)</script>", "123456", "用户名格式不正确"),
])
def test_boundary_values(self, username, password, expected):
"""边界值测试:空值、超长、特殊字符"""
self.login_page.login(username, password)
error_msg = self.login_page.get_error_message()
assert expected in error_msg, f'期望包含"{expected}",实际: "{error_msg}"'
4.5 第5步:页面跳转测试
测什么: 链接点击后能否正确跳转
python
# ========== 第5步:页面跳转测试 ==========
def test_register_link_jump(self):
"""点击注册链接 → 跳转到注册页"""
original_url = self.login_page.get_current_url()
self.login_page.click_register_link()
# 验证URL变了
assert self.login_page.get_current_url() != original_url
assert "register" in self.login_page.get_current_url()
# 验证页面内容正确
assert "用户注册" in self.login_page.get_page_title()
def test_forgot_password_link_jump(self):
"""点击找回密码链接 → 跳转到找回密码页"""
self.login_page.click_forgot_password_link()
assert "forgot" in self.login_page.get_current_url() or "reset" in self.login_page.get_current_url()
assert "找回密码" in self.login_page.get_page_title()
def test_back_button_after_jump(self):
"""跳转后浏览器后退 → 返回原页面"""
original_url = self.login_page.get_current_url()
self.login_page.click_register_link()
assert self.login_page.get_current_url() != original_url
self.login_page.driver.back()
assert self.login_page.get_current_url() == original_url
4.6 第6步:交互体验测试
测什么: 键盘操作、按钮状态、悬停效果等交互体验
python
# ========== 第6步:交互体验测试 ==========
def test_tab_key_navigation(self):
"""Tab键切换焦点"""
# 初始焦点在用户名框
username_input = self.login_page.find_element(self.login_page.USERNAME_INPUT)
assert self.login_page.driver.switch_to.active_element == username_input
# Tab到密码框
self.login_page.tab_to_password()
assert self.login_page.is_password_focused()
def test_enter_key_submit(self, valid_user):
"""Enter键提交登录"""
self.login_page.login_with_enter(valid_user["username"], valid_user["password"])
self.login_page.wait_for_url_contains("index")
assert self.login_page.is_login_success()
def test_button_disable_after_click(self):
"""点击后按钮变灰,防止重复提交"""
login_btn = self.login_page.find_element(self.login_page.LOGIN_BUTTON)
assert login_btn.is_enabled()
self.login_page.login("admin", "123456")
# 注意:实际项目中,点击后立即变灰,这里可能需要快速验证
# 或者测试"同意协议"按钮的置灰/启用状态
# assert not login_btn.is_enabled()
4.7 第7步:UI样式测试
测什么: 颜色、字体、间距、对齐是否符合设计稿
python
# ========== 第7步:UI样式测试 ==========
def test_login_button_color(self):
"""登录按钮颜色正确"""
color = self.login_page.get_login_button_color()
# 设计稿要求:#1890ff → RGB(24, 144, 255)
assert color == "rgb(24, 144, 255)" or color == "#1890ff", \
f"按钮颜色错误,期望: rgb(24,144,255),实际: {color}"
def test_password_masked(self):
"""密码框是掩码显示"""
assert self.login_page.is_password_masked(), "密码框应该显示为***"
def test_error_message_color(self):
"""错误提示文字是红色"""
self.login_page.login("admin", "wrong")
color = self.login_page.get_css_color(self.login_page.ERROR_MSG, "color")
assert color == "rgb(255, 0, 0)" or color == "#ff0000", \
f"错误提示颜色错误,应该是红色,实际: {color}"
def test_input_width_consistent(self):
"""输入框宽度一致"""
username_width = self.login_page.get_username_input_width()
password_width = self.login_page.get_password_input_width()
assert username_width == password_width, \
f"输入框宽度不一致:用户名框{username_width}px,密码框{password_width}px"
def test_input_spacing(self):
"""输入框间距正确"""
spacing = self.login_page.get_input_spacing()
# 设计稿要求:20px
assert spacing == 20, f"输入框间距错误,期望20px,实际{spacing}px"
def test_button_width(self):
"""按钮宽度正确"""
button_size = self.login_page.get_element_size(self.login_page.LOGIN_BUTTON)
assert button_size['width'] == 320, f"按钮宽度错误,期望320px,实际{button_size['width']}px"
assert button_size['height'] == 40, f"按钮高度错误,期望40px,实际{button_size['height']}px"
4.8 第8步:兼容性测试
测什么: 不同浏览器、不同分辨率下页面是否正常
python
# ========== 第8步:兼容性测试 ==========
@pytest.mark.parametrize("width,height,device", [
(1920, 1080, "桌面大屏"),
(1366, 768, "桌面小屏"),
(375, 667, "手机竖屏"),
(768, 1024, "平板竖屏"),
])
def test_responsive_layout(self, driver, base_url, width, height, device):
"""响应式布局测试"""
driver.set_window_size(width, height)
driver.get(base_url + "/login.html")
login_page = LoginPage(driver)
# 验证核心元素存在
assert login_page.has_username_input()
assert login_page.has_login_button()
# 验证没有横向滚动条(移动端除外)
if width >= 768:
scroll_width = driver.execute_script("return document.body.scrollWidth")
window_width = driver.execute_script("return window.innerWidth")
assert scroll_width <= window_width, f"{device}出现横向滚动条"
# 截图保存
login_page.take_screenshot(f"login_{device}_{width}x{height}")
def test_cross_browser_visual(self, driver, base_url):
"""多浏览器视觉效果(需要在conftest中配置多浏览器)"""
driver.get(base_url + "/login.html")
browser_name = driver.capabilities['browserName']
version = driver.capabilities.get('browserVersion', 'unknown')
login_page = LoginPage(driver)
# 验证核心功能
assert login_page.has_username_input()
# 截图保存,用于人工对比
login_page.take_screenshot(f"login_{browser_name}_{version}")
五、Pytest配置
5.1 conftest.py
python
# conftest.py
import pytest
import os
from datetime import datetime
from utils.driver_factory import DriverFactory
from utils.data_loader import DataLoader
from utils.logger import log
def pytest_addoption(parser):
"""添加命令行参数"""
parser.addoption("--browser", default="chrome", choices=["chrome", "firefox", "edge"])
parser.addoption("--headless", action="store_true", default=False)
@pytest.fixture(scope="session")
def driver(request):
"""浏览器驱动 fixture"""
browser = request.config.getoption("--browser")
headless = request.config.getoption("--headless")
driver = DriverFactory.get_driver(browser=browser, headless=headless)
yield driver
driver.quit()
@pytest.fixture(scope="session")
def base_url():
"""基础URL fixture"""
config = DataLoader.get_config()
return config["url"]["base_url"]
@pytest.fixture
def valid_user():
"""有效用户 fixture"""
data = DataLoader.get_test_data()
return data["valid_user"]
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""测试失败时自动截图"""
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
driver = item.funcargs.get("driver")
if driver:
screenshot_dir = "screenshots"
os.makedirs(screenshot_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{item.name}_{timestamp}.png"
filepath = os.path.join(screenshot_dir, filename)
driver.save_screenshot(filepath)
log.error(f"测试失败,截图已保存: {filepath}")
5.2 pytest.ini
ini
# pytest.ini
[pytest]
# 测试发现规则
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 命令行参数
addopts =
-v
-s
--strict-markers
--tb=short
# 自定义标记
markers =
p0: 核心功能测试
p1: 重要功能测试
p2: 次要功能测试
ui: UI样式测试
compatibility: 兼容性测试
# 测试目录
testpaths = testcases
# 日志配置
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s - %(levelname)s - %(message)s
log_cli_date_format = %H:%M:%S
六、运行测试
6.1 运行命令
bash
# 运行所有测试
pytest testcases/test_login.py -v -s
# 运行特定步骤(通过标记)
pytest testcases/test_login.py -m "not ui" -v -s
# 运行单个测试
pytest testcases/test_login.py::TestLogin::test_login_success -v -s
# 指定浏览器
pytest testcases/test_login.py --browser=firefox -v -s
# 无头模式运行
pytest testcases/test_login.py --headless -v -s
# 生成HTML报告
pytest testcases/test_login.py --html=reports/report.html --self-contained-html
6.2 测试结果
============================= test session starts =============================
collected 20 items
testcases/test_login.py::TestLogin::test_username_input_exists PASSED
testcases/test_login.py::TestLogin::test_password_input_exists PASSED
testcases/test_login.py::TestLogin::test_login_button_exists PASSED
testcases/test_login.py::TestLogin::test_login_success PASSED
testcases/test_login.py::TestLogin::test_login_fail[admin-wrong-密码错误] PASSED
testcases/test_login.py::TestLogin::test_boundary_values[--123456-请输入用户名] PASSED
...
============================= 20 passed in 45.23s =============================
七、总结
7.1 8步框架快速记忆
| 步骤 | 一句话 | 代码关键点 |
|---|---|---|
| 1 | 东西都在吗? | is_element_present() |
| 2 | 正确能成功吗? | login() + assert |
| 3 | 错误有提示吗? | get_error_message() |
| 4 | 极端能处理吗? | @parametrize 边界值 |
| 5 | 链接跳得对吗? | click + assert URL |
| 6 | 键盘鼠标顺滑吗? | send_keys(Keys.TAB) |
| 7 | 颜色布局对吗? | value_of_css_property() |
| 8 | 不同设备正常吗? | set_window_size() |
7.2 框架适用性
✅ 登录页 ✅ 注册页 ✅ 搜索页
✅ 商品列表 ✅ 商品详情 ✅ 购物车
✅ 订单页 ✅ 个人中心 ✅ 后台管理
任何有用户交互的Web页面都适用!
7.3 核心原则
- 先测基础,再测复杂(元素存在 → 功能 → 样式)
- 先测正常,再测异常(成功路径 → 失败路径)
- 自动化截图(失败时自动保留证据)
- 数据驱动(测试数据与代码分离)
这个框架可以让你拿到任何一个新页面时,都能系统化地设计测试用例,不会遗漏重要的测试点。